Class: Hyperion::Http2Handler::RequestStream

Inherits:
Protocol::HTTP2::Stream
  • Object
show all
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

Instance Method Summary collapse

Constructor Details

#initializeRequestStream

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_reasonObject (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_bodyObject (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_completeObject (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_headersObject (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!`.

Returns:

  • (Boolean)


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_windowObject

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