Class: Hyperion::Http2Handler::RequestStream
- Inherits:
-
Protocol::HTTP2::Stream
- Object
- Protocol::HTTP2::Stream
- Hyperion::Http2Handler::RequestStream
- Defined in:
- lib/hyperion/http2_handler.rb
Overview
Per-stream subclass that captures decoded request pseudo-headers, regular headers, and any DATA frame body bytes for later dispatch. Also exposes a ‘window_available` notification fan-out so the response-writer fiber can sleep until WINDOW_UPDATE arrives.
Constant Summary collapse
- VALID_REQUEST_PSEUDO_HEADERS =
RFC 7540 §8.1.2.1 — the only pseudo-headers a server MUST accept on a request. Anything else (notably ‘:status`, which is response-only, or an unknown `:foo`) is a malformed request that we reject with PROTOCOL_ERROR.
%w[:method :path :scheme :authority].freeze
- FORBIDDEN_HEADERS =
RFC 7540 §8.1.2.2 — these connection-specific headers MUST NOT appear in HTTP/2 requests; their semantics are folded into HTTP/2 framing.
%w[connection transfer-encoding keep-alive upgrade proxy-connection].freeze
Instance Attribute Summary collapse
-
#protocol_error_reason ⇒ Object
readonly
Returns the value of attribute protocol_error_reason.
-
#request_body ⇒ Object
readonly
Returns the value of attribute request_body.
-
#request_complete ⇒ Object
readonly
Returns the value of attribute request_complete.
-
#request_headers ⇒ Object
readonly
Returns the value of attribute request_headers.
Instance Method Summary collapse
-
#initialize ⇒ RequestStream
constructor
A new instance of RequestStream.
- #process_data(frame) ⇒ Object
- #process_headers(frame) ⇒ Object
-
#protocol_error? ⇒ Boolean
Used by the dispatch loop to decide whether to invoke the app or send RST_STREAM PROTOCOL_ERROR.
-
#validate_body_length! ⇒ Object
RFC 7540 §8.1.2.6 — if ‘content-length` was advertised, the actual number of DATA bytes received (across all DATA frames) MUST match.
-
#validate_request_headers! ⇒ Object
RFC 7540 §8.1.2 — request header validation.
-
#wait_for_window ⇒ Object
Block the calling fiber until the remote window grows.
-
#window_updated(size) ⇒ Object
Called by protocol-http2 whenever the remote peer’s flow-control window opens up — either via a stream-level WINDOW_UPDATE or via the connection-level fan-out in ‘Connection#consume_window`.
Constructor Details
#initialize ⇒ RequestStream
Returns a new instance of RequestStream.
51 52 53 54 55 56 57 58 59 60 |
# File 'lib/hyperion/http2_handler.rb', line 51 def initialize(*) super @request_headers = [] @request_body = +'' @request_body_bytes = 0 @request_complete = false @window_available = ::Async::Notification.new @protocol_error_reason = nil @declared_content_length = nil end |
Instance Attribute Details
#protocol_error_reason ⇒ Object (readonly)
Returns the value of attribute protocol_error_reason.
49 50 51 |
# File 'lib/hyperion/http2_handler.rb', line 49 def protocol_error_reason @protocol_error_reason end |
#request_body ⇒ Object (readonly)
Returns the value of attribute request_body.
49 50 51 |
# File 'lib/hyperion/http2_handler.rb', line 49 def request_body @request_body end |
#request_complete ⇒ Object (readonly)
Returns the value of attribute request_complete.
49 50 51 |
# File 'lib/hyperion/http2_handler.rb', line 49 def request_complete @request_complete end |
#request_headers ⇒ Object (readonly)
Returns the value of attribute request_headers.
49 50 51 |
# File 'lib/hyperion/http2_handler.rb', line 49 def request_headers @request_headers end |
Instance Method Details
#process_data(frame) ⇒ Object
90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/hyperion/http2_handler.rb', line 90 def process_data(frame) data = super # rubocop:disable Rails/Present if data && !data.empty? @request_body << data @request_body_bytes += data.bytesize end # rubocop:enable Rails/Present if frame.end_stream? validate_body_length! unless protocol_error? @request_complete = true end data end |
#process_headers(frame) ⇒ Object
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/hyperion/http2_handler.rb', line 69 def process_headers(frame) decoded = super # First HEADERS frame on a stream carries the request header block; # any later HEADERS frame is trailers (§8.1) and we deliberately do # not re-validate (re-running the validator would see the original # request pseudo-headers plus the new trailer block and falsely flag # them as misordered). first_block = @request_headers.empty? # decoded is an Array of [name, value] pairs (HPACK output). decoded.each { |pair| @request_headers << pair } # Run RFC 7540 §8.1.2 validation as soon as we have a complete header # block. We do it here (not at end_stream) so the dispatcher sees the # error flag before it spawns a fiber for the request. validate_request_headers! if first_block && !protocol_error? if frame.end_stream? validate_body_length! unless protocol_error? @request_complete = true end decoded end |
#protocol_error? ⇒ Boolean
Used by the dispatch loop to decide whether to invoke the app or send RST_STREAM PROTOCOL_ERROR. Set by ‘validate_request_headers!` and `validate_body_length!`.
65 66 67 |
# File 'lib/hyperion/http2_handler.rb', line 65 def protocol_error? !@protocol_error_reason.nil? end |
#validate_body_length! ⇒ Object
RFC 7540 §8.1.2.6 — if ‘content-length` was advertised, the actual number of DATA bytes received (across all DATA frames) MUST match.
167 168 169 170 171 172 173 174 |
# File 'lib/hyperion/http2_handler.rb', line 167 def validate_body_length! return if @declared_content_length.nil? return if @declared_content_length == @request_body_bytes fail_validation!( "content-length mismatch: declared #{@declared_content_length}, received #{@request_body_bytes}" ) end |
#validate_request_headers! ⇒ Object
RFC 7540 §8.1.2 — request header validation. Sets ‘@protocol_error_reason` on the first violation we hit; the dispatch loop turns that into RST_STREAM PROTOCOL_ERROR.
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'lib/hyperion/http2_handler.rb', line 108 def validate_request_headers! seen_regular = false pseudo_counts = Hash.new(0) @request_headers.each do |pair| name, value = pair name = name.to_s if name.start_with?(':') # §8.1.2.1: pseudo-headers MUST precede regular headers. return fail_validation!('pseudo-header after regular header') if seen_regular # §8.1.2.1: only the four request pseudo-headers are valid; in # particular, `:status` is response-only. unless VALID_REQUEST_PSEUDO_HEADERS.include?(name) return fail_validation!("invalid request pseudo-header: #{name}") end pseudo_counts[name] += 1 else seen_regular = true # §8.1.2: header names must be lowercase in HTTP/2. return fail_validation!('uppercase header name') if /[A-Z]/.match?(name) # §8.1.2.2: connection-specific headers are forbidden. return fail_validation!("forbidden connection-specific header: #{name}") if FORBIDDEN_HEADERS.include?(name) # §8.1.2.2: TE may only carry the value `trailers`. if name == 'te' && value.to_s.downcase.strip != 'trailers' return fail_validation!('TE header with non-trailers value') end # Track declared content-length for later body-byte cross-check. @declared_content_length = value.to_s.to_i if name == 'content-length' end end # §8.1.2.3: every pseudo-header may appear at most once. pseudo_counts.each do |name, count| return fail_validation!("duplicated pseudo-header: #{name}") if count > 1 end method = pseudo_value(':method') # CONNECT (§8.3) has its own rules; everything else MUST carry # :method, :scheme and a non-empty :path. if method == 'CONNECT' return fail_validation!('CONNECT with :scheme') if pseudo_value(':scheme') return fail_validation!('CONNECT with :path') if pseudo_value(':path') return fail_validation!('CONNECT without :authority') unless pseudo_value(':authority') else return fail_validation!('missing :method') if method.nil? || method.empty? scheme = pseudo_value(':scheme') return fail_validation!('missing :scheme') if scheme.nil? || scheme.empty? path = pseudo_value(':path') return fail_validation!('missing or empty :path') if path.nil? || path.empty? end nil end |
#wait_for_window ⇒ Object
Block the calling fiber until the remote window grows. Cheap no-op signal each time ‘window_updated` fires; the caller re-checks available_frame_size in a loop.
188 189 190 |
# File 'lib/hyperion/http2_handler.rb', line 188 def wait_for_window @window_available.wait end |
#window_updated(size) ⇒ Object
Called by protocol-http2 whenever the remote peer’s flow-control window opens up — either via a stream-level WINDOW_UPDATE or via the connection-level fan-out in ‘Connection#consume_window`. We poke the notification so any fiber waiting in `wait_for_window` resumes.
180 181 182 183 |
# File 'lib/hyperion/http2_handler.rb', line 180 def window_updated(size) @window_available.signal super end |