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