lib/http/2/stream.rb in http-2-0.7.0 vs lib/http/2/stream.rb in http-2-0.8.0
- old
+ new
@@ -1,7 +1,6 @@
module HTTP2
-
# A single HTTP 2.0 connection can multiplex multiple streams in parallel:
# multiple requests and responses can be in flight simultaneously and stream
# data can be interleaved and prioritized.
#
# This class encapsulates all of the state, transition, flow-control, and
@@ -53,11 +52,11 @@
attr_reader :dependency
# Size of current stream flow control window.
attr_reader :local_window
attr_reader :remote_window
- alias :window :local_window
+ alias_method :window, :local_window
# Reason why connection was closed.
attr_reader :closed
# Initializes new stream.
@@ -70,16 +69,17 @@
# @param weight [Integer]
# @param dependency [Integer]
# @param exclusive [Boolean]
# @param window [Integer]
# @param parent [Stream]
- def initialize(connection: nil, id: nil, weight: 16, dependency: 0, exclusive: false, parent: nil)
- @connection = connection or raise ArgumentError.new("missing mandatory argument connection")
- @id = id or raise ArgumentError.new("missing mandatory argument id")
+ def initialize(connection:, id:, weight: 16, dependency: 0, exclusive: false, parent: nil)
+ @connection = connection
+ @id = id
@weight = weight
@dependency = dependency
- process_priority({weight: weight, stream_dependency: dependency, exclusive: exclusive})
+ process_priority(weight: weight, stream_dependency: dependency, exclusive: exclusive)
+ @local_window = connection.local_settings[:settings_initial_window_size]
@remote_window = connection.remote_settings[:settings_initial_window_size]
@parent = parent
@state = :idle
@error = false
@closed = false
@@ -95,26 +95,25 @@
def receive(frame)
transition(frame, false)
case frame[:type]
when :data
- # TODO: when receiving DATA, keep track of local_window.
- emit(:data, frame[:payload]) if !frame[:ignore]
+ @local_window -= frame[:payload].size
+ emit(:data, frame[:payload]) unless frame[:ignore]
when :headers, :push_promise
- emit(:headers, frame[:payload]) if !frame[:ignore]
+ emit(:headers, frame[:payload]) unless frame[:ignore]
when :priority
process_priority(frame)
when :window_update
- @remote_window += frame[:increment]
- send_data
+ process_window_update(frame)
when :altsvc, :blocked
emit(frame[:type], frame)
end
complete_transition(frame)
end
- alias :<< :receive
+ alias_method :<<, :receive
# Processes outgoing HTTP 2.0 frames. Data frames may be automatically
# split and buffered based on maximum frame size and current stream flow
# control window size.
#
@@ -123,12 +122,17 @@
transition(frame, true)
frame[:stream] ||= @id
process_priority(frame) if frame[:type] == :priority
- if frame[:type] == :data
+ case frame[:type]
+ when :data
+ # @remote_window is maintained in send_data
send_data(frame)
+ when :window_update
+ @local_window += frame[:increment]
+ emit(:frame, frame)
else
emit(:frame, frame)
end
complete_transition(frame)
@@ -142,15 +146,15 @@
def headers(headers, end_headers: true, end_stream: false)
flags = []
flags << :end_headers if end_headers
flags << :end_stream if end_stream
- send({type: :headers, flags: flags, payload: headers.to_a})
+ send(type: :headers, flags: flags, payload: headers.to_a)
end
def promise(headers, end_headers: true, &block)
- raise Exception.new("must provide callback") if !block_given?
+ fail ArgumentError, 'must provide callback' unless block_given?
flags = end_headers ? [:end_headers] : []
emit(:promise, self, headers, flags, &block)
end
@@ -159,96 +163,104 @@
#
# @param weight [Integer] new stream weight value
# @param dependency [Integer] new stream dependency stream
def reprioritize(weight: 16, dependency: 0, exclusive: false)
stream_error if @id.even?
- send({type: :priority, weight: weight, stream_dependency: dependency, exclusive: exclusive})
+ send(type: :priority, weight: weight, stream_dependency: dependency, exclusive: exclusive)
end
# Sends DATA frame containing response payload.
#
# @param payload [String]
# @param end_stream [Boolean] indicates last response DATA frame
def data(payload, end_stream: true)
- flags = []
- flags << :end_stream if end_stream
-
# Split data according to each frame is smaller enough
# TODO: consider padding?
max_size = @connection.remote_settings[:settings_max_frame_size]
- while payload.bytesize > max_size do
+ while payload.bytesize > max_size
chunk = payload.slice!(0, max_size)
- send({type: :data, payload: chunk})
+ send(type: :data, flags: [], payload: chunk)
end
- send({type: :data, flags: flags, payload: payload})
+ flags = []
+ flags << :end_stream if end_stream
+ send(type: :data, flags: flags, payload: payload)
end
# Sends a RST_STREAM frame which closes current stream - this does not
# close the underlying connection.
#
# @param error [:Symbol] optional reason why stream was closed
def close(error = :stream_closed)
- send({type: :rst_stream, error: error})
+ send(type: :rst_stream, error: error)
end
# Sends a RST_STREAM indicating that the stream is no longer needed.
def cancel
- send({type: :rst_stream, error: :cancel})
+ send(type: :rst_stream, error: :cancel)
end
# Sends a RST_STREAM indicating that the stream has been refused prior
# to performing any application processing.
def refuse
- send({type: :rst_stream, error: :refused_stream})
+ send(type: :rst_stream, error: :refused_stream)
end
private
# HTTP 2.0 Stream States
- # - http://tools.ietf.org/html/draft-ietf-httpbis-http2-05#section-5
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-5.1
#
- # +--------+
- # PP | | PP
- # ,--------| idle |--------.
- # / | | \
- # v +--------+ v
- # +----------+ | +----------+
- # | | | H | |
- # ,---| reserved | | | reserved |---.
- # | | (local) | v | (remote) | |
- # | +----------+ +--------+ +----------+ |
- # | | ES | | ES | |
- # | | H ,-------| open |-------. | H |
- # | | / | | \ | |
- # | v v +--------+ v v |
- # | +----------+ | +----------+ |
- # | | half | | | half | |
- # | | closed | | R | closed | |
- # | | (remote) | | | (local) | |
- # | +----------+ | +----------+ |
- # | | v | |
- # | | ES / R +--------+ ES / R | |
- # | `----------->| |<-----------' |
- # | R | closed | R |
- # `-------------------->| |<--------------------'
- # +--------+
+ # +--------+
+ # send PP | | recv PP
+ # ,--------| idle |--------.
+ # / | | \
+ # v +--------+ v
+ # +----------+ | +----------+
+ # | | | send H/ | |
+ # ,-----| reserved | | recv H | reserved |-----.
+ # | | (local) | | | (remote) | |
+ # | +----------+ v +----------+ |
+ # | | +--------+ | |
+ # | | recv ES | | send ES | |
+ # | send H | ,-------| open |-------. | recv H |
+ # | | / | | \ | |
+ # | v v +--------+ v v |
+ # | +----------+ | +----------+ |
+ # | | half | | | half | |
+ # | | closed | | send R/ | closed | |
+ # | | (remote) | | recv R | (local) | |
+ # | +----------+ | +----------+ |
+ # | | | | |
+ # | | send ES/ | recv ES/ | |
+ # | | send R/ v send R/ | |
+ # | | recv R +--------+ recv R | |
+ # | send R/ `----------->| |<-----------' send R/ |
+ # | recv R | closed | recv R |
+ # `---------------------->| |<----------------------'
+ # +--------+
#
def transition(frame, sending)
case @state
# All streams start in the "idle" state. In this state, no frames
# have been exchanged.
+ # The following transitions are valid from this state:
# * Sending or receiving a HEADERS frame causes the stream to
# become "open". The stream identifier is selected as described
- # in Section 5.1.1.
- # * Sending a PUSH_PROMISE frame marks the associated stream for
+ # in Section 5.1.1. The same HEADERS frame can also cause a
+ # stream to immediately become "half closed".
+ # * Sending a PUSH_PROMISE frame reserves an idle stream for later
+ # use. The stream state for the reserved stream transitions to
+ # "reserved (local)".
+ # * Receiving a PUSH_PROMISE frame reserves an idle stream for
# later use. The stream state for the reserved stream
- # transitions to "reserved (local)".
- # * Receiving a PUSH_PROMISE frame marks the associated stream as
- # reserved by the remote peer. The state of the stream becomes
- # "reserved (remote)".
+ # transitions to "reserved (remote)".
+ # Receiving any frames other than HEADERS, PUSH_PROMISE or PRIORITY
+ # on a stream in this state MUST be treated as a connection error
+ # (Section 5.4.1) of type PROTOCOL_ERROR.
+
when :idle
if sending
case frame[:type]
when :push_promise then event(:reserved_local)
when :headers
@@ -256,82 +268,97 @@
event(:half_closed_local)
else
event(:open)
end
when :rst_stream then event(:local_rst)
- else stream_error; end
+ when :priority then process_priority(frame)
+ else stream_error
+ end
else
case frame[:type]
when :push_promise then event(:reserved_remote)
when :headers
if end_stream?(frame)
event(:half_closed_remote)
else
event(:open)
end
- else stream_error(:protocol_error); end
+ when :priority then process_priority(frame)
+ else stream_error(:protocol_error)
+ end
end
# A stream in the "reserved (local)" state is one that has been
# promised by sending a PUSH_PROMISE frame. A PUSH_PROMISE frame
# reserves an idle stream by associating the stream with an open
# stream that was initiated by the remote peer (see Section 8.2).
+ # In this state, only the following transitions are possible:
# * The endpoint can send a HEADERS frame. This causes the stream
# to open in a "half closed (remote)" state.
# * Either endpoint can send a RST_STREAM frame to cause the stream
- # to become "closed". This also releases the stream reservation.
- # An endpoint MUST NOT send any other type of frame in this state.
- # Receiving any frame other than RST_STREAM or PRIORITY MUST be
- # treated as a connection error (Section 5.4.1) of type
- # PROTOCOL_ERROR.
+ # to become "closed". This releases the stream reservation.
+ # An endpoint MUST NOT send any type of frame other than HEADERS,
+ # RST_STREAM, or PRIORITY in this state.
+ # A PRIORITY or WINDOW_UPDATE frame MAY be received in this state.
+ # Receiving any type of frame other than RST_STREAM, PRIORITY or
+ # WINDOW_UPDATE on a stream in this state MUST be treated as a
+ # connection error (Section 5.4.1) of type PROTOCOL_ERROR.
when :reserved_local
if sending
@state = case frame[:type]
when :headers then event(:half_closed_remote)
when :rst_stream then event(:local_rst)
- else stream_error; end
+ else stream_error
+ end
else
@state = case frame[:type]
- when :rst_stream then event(:remote_rst)
- when :priority then @state
- else stream_error; end
+ when :rst_stream then event(:remote_rst)
+ when :priority, :window_update then @state
+ else stream_error
+ end
end
# A stream in the "reserved (remote)" state has been reserved by a
# remote peer.
+ # In this state, only the following transitions are possible:
# * Receiving a HEADERS frame causes the stream to transition to
# "half closed (local)".
# * Either endpoint can send a RST_STREAM frame to cause the stream
- # to become "closed". This also releases the stream reservation.
- # Receiving any other type of frame MUST be treated as a stream
- # error (Section 5.4.2) of type PROTOCOL_ERROR. An endpoint MAY
- # send RST_STREAM or PRIORITY frames in this state to cancel or
- # reprioritize the reserved stream.
+ # to become "closed". This releases the stream reservation.
+ # An endpoint MAY send a PRIORITY frame in this state to
+ # reprioritize the reserved stream. An endpoint MUST NOT send any
+ # type of frame other than RST_STREAM, WINDOW_UPDATE, or PRIORITY in
+ # this state.
+ # Receiving any type of frame other than HEADERS, RST_STREAM or
+ # PRIORITY on a stream in this state MUST be treated as a connection
+ # error (Section 5.4.1) of type PROTOCOL_ERROR.
when :reserved_remote
if sending
@state = case frame[:type]
when :rst_stream then event(:local_rst)
- when :priority then @state
- else stream_error; end
+ when :priority, :window_update then @state
+ else stream_error
+ end
else
@state = case frame[:type]
- when :headers then event(:half_closed_local)
- when :rst_stream then event(:remote_rst)
- else stream_error; end
+ when :headers then event(:half_closed_local)
+ when :rst_stream then event(:remote_rst)
+ else stream_error
+ end
end
- # The "open" state is where both peers can send frames of any type.
- # In this state, sending peers observe advertised stream level flow
- # control limits (Section 5.2).
- # * From this state either endpoint can send a frame with a END_STREAM
- # flag set, which causes the stream to transition into one of the
- # "half closed" states: an endpoint sending a END_STREAM flag causes
- # the stream state to become "half closed (local)"; an endpoint
- # receiving a END_STREAM flag causes the stream state to become
- # "half closed (remote)".
- # * Either endpoint can send a RST_STREAM frame from this state,
- # causing it to transition immediately to "closed".
+ # A stream in the "open" state may be used by both peers to send
+ # frames of any type. In this state, sending peers observe
+ # advertised stream level flow control limits (Section 5.2).
+ # From this state either endpoint can send a frame with an
+ # END_STREAM flag set, which causes the stream to transition into
+ # one of the "half closed" states: an endpoint sending an END_STREAM
+ # flag causes the stream state to become "half closed (local)"; an
+ # endpoint receiving an END_STREAM flag causes the stream state to
+ # become "half closed (remote)".
+ # Either endpoint can send a RST_STREAM frame from this state,
+ # causing it to transition immediately to "closed".
when :open
if sending
case frame[:type]
when :data, :headers, :continuation
event(:half_closed_local) if end_stream?(frame)
@@ -343,25 +370,30 @@
event(:half_closed_remote) if end_stream?(frame)
when :rst_stream then event(:remote_rst)
end
end
- # A stream that is "half closed (local)" cannot be used for sending
- # frames.
+ # A stream that is in the "half closed (local)" state cannot be used
+ # for sending frames. Only WINDOW_UPDATE, PRIORITY and RST_STREAM
+ # frames can be sent in this state.
# A stream transitions from this state to "closed" when a frame that
- # contains a END_STREAM flag is received, or when either peer sends
+ # contains an END_STREAM flag is received, or when either peer sends
# a RST_STREAM frame.
- # A receiver can ignore WINDOW_UPDATE or PRIORITY frames in this
- # state. These frame types might arrive for a short period after a
- # frame bearing the END_STREAM flag is sent.
+ # A receiver can ignore WINDOW_UPDATE frames in this state, which
+ # might arrive for a short period after a frame bearing the
+ # END_STREAM flag is sent.
+ # PRIORITY frames received in this state are used to reprioritize
+ # streams that depend on the current stream.
when :half_closed_local
if sending
case frame[:type]
when :rst_stream
event(:local_rst)
when :priority
process_priority(frame)
+ when :window_update
+ # nop here
else
stream_error
end
else
case frame[:type]
@@ -369,78 +401,108 @@
event(:remote_closed) if end_stream?(frame)
when :rst_stream then event(:remote_rst)
when :priority
process_priority(frame)
when :window_update
- frame[:ignore] = true
+ # nop here
end
end
# A stream that is "half closed (remote)" is no longer being used by
# the peer to send frames. In this state, an endpoint is no longer
# obligated to maintain a receiver flow control window if it
# performs flow control.
# If an endpoint receives additional frames for a stream that is in
- # this state it MUST respond with a stream error (Section 5.4.2) of
- # type STREAM_CLOSED.
+ # this state, other than WINDOW_UPDATE, PRIORITY or RST_STREAM, it
+ # MUST respond with a stream error (Section 5.4.2) of type
+ # STREAM_CLOSED.
+ # A stream that is "half closed (remote)" can be used by the
+ # endpoint to send frames of any type. In this state, the endpoint
+ # continues to observe advertised stream level flow control limits
+ # (Section 5.2).
# A stream can transition from this state to "closed" by sending a
- # frame that contains a END_STREAM flag, or when either peer sends a
- # RST_STREAM frame.
+ # frame that contains an END_STREAM flag, or when either peer sends
+ # a RST_STREAM frame.
when :half_closed_remote
if sending
case frame[:type]
when :data, :headers, :continuation
event(:local_closed) if end_stream?(frame)
when :rst_stream then event(:local_rst)
end
else
case frame[:type]
when :rst_stream then event(:remote_rst)
- when :window_update then frame[:ignore] = true
when :priority
process_priority(frame)
- else stream_error(:stream_closed); end
+ when :window_update
+ # nop
+ else
+ stream_error(:stream_closed)
+ end
end
- # An endpoint MUST NOT send frames on a closed stream. An endpoint
- # that receives a frame after receiving a RST_STREAM or a frame
- # containing a END_STREAM flag on that stream MUST treat that as a
- # stream error (Section 5.4.2) of type STREAM_CLOSED.
- #
- # WINDOW_UPDATE or PRIORITY frames can be received in this state for
- # a short period after a a frame containing an END_STREAM flag is
- # sent. Until the remote peer receives and processes the frame
- # bearing the END_STREAM flag, it might send either frame type.
- #
+ # The "closed" state is the terminal state.
+ # An endpoint MUST NOT send frames other than PRIORITY on a closed
+ # stream. An endpoint that receives any frame other than PRIORITY
+ # after receiving a RST_STREAM MUST treat that as a stream error
+ # (Section 5.4.2) of type STREAM_CLOSED. Similarly, an endpoint
+ # that receives any frames after receiving a frame with the
+ # END_STREAM flag set MUST treat that as a connection error
+ # (Section 5.4.1) of type STREAM_CLOSED, unless the frame is
+ # permitted as described below.
+ # WINDOW_UPDATE or RST_STREAM frames can be received in this state
+ # for a short period after a DATA or HEADERS frame containing an
+ # END_STREAM flag is sent. Until the remote peer receives and
+ # processes RST_STREAM or the frame bearing the END_STREAM flag, it
+ # might send frames of these types. Endpoints MUST ignore
+ # WINDOW_UPDATE or RST_STREAM frames received in this state, though
+ # endpoints MAY choose to treat frames that arrive a significant
+ # time after sending END_STREAM as a connection error
+ # (Section 5.4.1) of type PROTOCOL_ERROR.
+ # PRIORITY frames can be sent on closed streams to prioritize
+ # streams that are dependent on the closed stream. Endpoints SHOULD
+ # process PRIORITY frames, though they can be ignored if the stream
+ # has been removed from the dependency tree (see Section 5.3.4).
# If this state is reached as a result of sending a RST_STREAM
# frame, the peer that receives the RST_STREAM might have already
# sent - or enqueued for sending - frames on the stream that cannot
- # be withdrawn. An endpoint MUST ignore frames that it receives on
- # closed streams after it has sent a RST_STREAM frame.
- #
- # An endpoint might receive a PUSH_PROMISE or a CONTINUATION frame
- # after it sends RST_STREAM. PUSH_PROMISE causes a stream to become
- # "reserved". If promised streams are not desired, a RST_STREAM can
- # be used to close any of those streams.
+ # be withdrawn. An endpoint MUST ignore frames that it receives on
+ # closed streams after it has sent a RST_STREAM frame. An endpoint
+ # MAY choose to limit the period over which it ignores frames and
+ # treat frames that arrive after this time as being in error.
+ # Flow controlled frames (i.e., DATA) received after sending
+ # RST_STREAM are counted toward the connection flow control window.
+ # Even though these frames might be ignored, because they are sent
+ # before the sender receives the RST_STREAM, the sender will
+ # consider the frames to count against the flow control window.
+ # An endpoint might receive a PUSH_PROMISE frame after it sends
+ # RST_STREAM. PUSH_PROMISE causes a stream to become "reserved"
+ # even if the associated stream has been reset. Therefore, a
+ # RST_STREAM is needed to close an unwanted promised stream.
when :closed
if sending
case frame[:type]
when :rst_stream then # ignore
when :priority then
process_priority(frame)
else
- stream_error(:stream_closed) if !(frame[:type] == :rst_stream)
+ stream_error(:stream_closed) unless (frame[:type] == :rst_stream)
end
else
if frame[:type] == :priority
process_priority(frame)
else
case @closed
when :remote_rst, :remote_closed
- stream_error(:stream_closed) if !(frame[:type] == :rst_stream)
+ case frame[:type]
+ when :rst_stream, :window_update # nop here
+ else
+ stream_error(:stream_closed)
+ end
when :local_rst, :local_closed
- frame[:ignore] = true
+ frame[:ignore] = true if frame[:type] != :window_update
end
end
end
end
end
@@ -480,32 +542,35 @@
end
def process_priority(frame)
@weight = frame[:weight]
@dependency = frame[:stream_dependency]
- emit(:priority,
- weight: frame[:weight],
- dependency: frame[:stream_dependency],
- exclusive: frame[:exclusive])
+ emit(
+ :priority,
+ weight: frame[:weight],
+ dependency: frame[:stream_dependency],
+ exclusive: frame[:exclusive],
+ )
# TODO: implement dependency tree housekeeping
# Latest draft defines a fairly complex priority control.
- # See https://tools.ietf.org/html/draft-ietf-httpbis-http2-14#section-5.3
+ # See https://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-5.3
# We currently have no prioritization among streams.
# We should add code here.
end
def end_stream?(frame)
case frame[:type]
when :data, :headers, :continuation
frame[:flags].include?(:end_stream)
- else false; end
+ else false
+ end
end
- def stream_error(error = :stream_error, msg: nil)
+ def stream_error(error = :internal_error, msg: nil)
@error = error
close(error) if @state != :closed
klass = error.to_s.split('_').map(&:capitalize).join
- raise Error.const_get(klass).new(msg)
+ fail Error.const_get(klass), msg
end
end
end