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.
227 228 229 230 231 232 233 234 235 236 |
# File 'lib/hyperion/http2_handler.rb', line 227 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.
225 226 227 |
# File 'lib/hyperion/http2_handler.rb', line 225 def protocol_error_reason @protocol_error_reason end |
#request_body ⇒ Object (readonly)
Returns the value of attribute request_body.
225 226 227 |
# File 'lib/hyperion/http2_handler.rb', line 225 def request_body @request_body end |
#request_complete ⇒ Object (readonly)
Returns the value of attribute request_complete.
225 226 227 |
# File 'lib/hyperion/http2_handler.rb', line 225 def request_complete @request_complete end |
#request_headers ⇒ Object (readonly)
Returns the value of attribute request_headers.
225 226 227 |
# File 'lib/hyperion/http2_handler.rb', line 225 def request_headers @request_headers end |
Instance Method Details
#process_data(frame) ⇒ Object
266 267 268 269 270 271 272 273 274 275 276 277 278 279 |
# File 'lib/hyperion/http2_handler.rb', line 266 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
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 |
# File 'lib/hyperion/http2_handler.rb', line 245 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!`.
241 242 243 |
# File 'lib/hyperion/http2_handler.rb', line 241 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.
343 344 345 346 347 348 349 350 |
# File 'lib/hyperion/http2_handler.rb', line 343 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.
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 |
# File 'lib/hyperion/http2_handler.rb', line 284 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.
364 365 366 |
# File 'lib/hyperion/http2_handler.rb', line 364 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.
356 357 358 359 |
# File 'lib/hyperion/http2_handler.rb', line 356 def window_updated(size) @window_available.signal super end |