Class: Raptor::Http2
- Inherits:
-
Object
- Object
- Raptor::Http2
- Defined in:
- lib/raptor/http2.rb,
sig/generated/raptor/http2.rbs
Overview
Handles HTTP/2 request processing and Rack application integration.
Defined Under Namespace
Classes: FlowControl, Writer
Constant Summary collapse
- EAGER_READ_TIMEOUT =
0.001- EAGER_READ_BUFFER_SIZE =
64 * 1024
- EAGER_MAX_ROUNDS =
8- FLAG_END_STREAM =
0x1- FLAG_END_HEADERS =
0x4- FLAG_ACK =
0x1- FLAG_PRIORITY =
0x20- ERROR_NO_ERROR =
0x0- ERROR_PROTOCOL_ERROR =
0x1- DEFAULT_WINDOW_SIZE =
65_535- MAX_FRAME_SIZE =
16_384- SERVER_PROTOCOL =
"HTTP/2"- REQUEST_PSEUDO_HEADERS =
[":method", ":scheme", ":path", ":authority"].freeze
- REQUIRED_REQUEST_PSEUDO_HEADERS =
[":method", ":scheme", ":path"].freeze
Instance Attribute Summary collapse
-
#initial_settings_frame ⇒ String
readonly
Returns the initial server SETTINGS frame to send on every new HTTP/2 connection.
Class Method Summary collapse
-
.invalid_pseudo_headers?(headers) ⇒ Boolean
Returns true when a decoded header block violates the HTTP/2 pseudo-header rules from RFC 9113 section 8.3: an unknown pseudo-header, a duplicate pseudo-header, a pseudo-header appearing after any regular header, or a required pseudo-header (
:method,:scheme,:path) missing on non-CONNECTrequests. -
.process_frames(data) ⇒ Hash
Advances HTTP/2 frame parsing from the connection buffer, returning updated connection state along with any outgoing protocol frames and completed stream requests.
Instance Method Summary collapse
-
#apply_flow_control_updates(flow_control, result) ⇒ void
Applies inbound flow-control updates from a parsed result to the connection's
FlowControl. -
#build_rack_env(headers, body, remote_addr:) ⇒ Hash
Builds a Rack environment hash from HTTP/2 headers and body.
-
#create_writer ⇒ Writer
Creates a per-connection Writer configured with the handler's write timeout.
-
#dispatch_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:) ⇒ void
Dispatches a completed stream request to the Rack app and writes the response back as HTTP/2 frames.
-
#eager_accept(socket, id, reactor, thread_pool, remote_addr, url_scheme) ⇒ void
Sends the server SETTINGS frame on a freshly negotiated HTTP/2 connection, then eagerly reads and parses the first client frame batch, dispatching completed streams directly to the thread pool.
-
#eager_read_next_batch(socket) ⇒ String?
Reads the next frame batch from
socketwithin a short window, or returns nil if nothing arrives in time. -
#handle_parsed_request(result, reactor, thread_pool) ⇒ void
Handles a parsed HTTP/2 result.
-
#initialize(app, server_port, connection_options: {}, http2_options: {}, access_log_io: nil, on_error: nil) ⇒ Http2
constructor
Creates a new Http2 handler.
-
#parser_worker ⇒ Proc
Returns a Ractor-safe proc that parses HTTP/2 frames from the state hash's buffered bytes.
-
#populate_server_name_and_port(env) ⇒ void
Populates SERVER_NAME and SERVER_PORT from the HTTP_HOST header.
-
#write_access_log(env, status, size, remote_addr) ⇒ void
Instance-level wrapper around Raptor::Http.write_access_log that routes to the configured
@access_log_io. -
#write_http2_error_response(socket, writer, stream_id) ⇒ void
Writes a 500 error response as HTTP/2 frames.
-
#write_http2_response(socket, writer, flow_control, stream_id, status, headers, body) ⇒ String
Writes a Rack response as HTTP/2 frames to the socket, partitioning DATA frames through
flow_controlto fit within the peer's windows.
Constructor Details
#initialize(app, server_port, connection_options: {}, http2_options: {}, access_log_io: nil, on_error: nil) ⇒ Http2
Creates a new Http2 handler.
292 293 294 295 296 297 298 299 300 301 302 303 304 305 |
# File 'lib/raptor/http2.rb', line 292 def initialize(app, server_port, connection_options: {}, http2_options: {}, access_log_io: nil, on_error: nil) @app = app @server_port = server_port @write_timeout = [:write_timeout] || Http::WRITE_TIMEOUT @access_log_io = access_log_io @on_error = on_error parser = Http2Parser.new settings_payload = parser.build_settings( max_concurrent_streams: [:max_concurrent_streams], initial_window_size: DEFAULT_WINDOW_SIZE ) @initial_settings_frame = parser.build_frame(:settings, 0, 0, settings_payload).freeze end |
Instance Attribute Details
#initial_settings_frame ⇒ String (readonly)
Returns the initial server SETTINGS frame to send on every new HTTP/2 connection.
277 278 279 |
# File 'lib/raptor/http2.rb', line 277 def initial_settings_frame @initial_settings_frame end |
Class Method Details
.invalid_pseudo_headers?(headers) ⇒ Boolean
Returns true when a decoded header block violates the HTTP/2 pseudo-header
rules from RFC 9113 section 8.3: an unknown pseudo-header, a duplicate
pseudo-header, a pseudo-header appearing after any regular header, or a
required pseudo-header (:method, :scheme, :path) missing on
non-CONNECT requests.
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 |
# File 'lib/raptor/http2.rb', line 246 def self.invalid_pseudo_headers?(headers) seen_pseudo = {} seen_regular = false headers.each do |name, _value| if name.start_with?(":") return true if seen_regular return true unless REQUEST_PSEUDO_HEADERS.include?(name) return true if seen_pseudo[name] seen_pseudo[name] = true else seen_regular = true end end return false if seen_pseudo[":method"] && headers.assoc(":method")&.last == "CONNECT" REQUIRED_REQUEST_PSEUDO_HEADERS.any? { |name| !seen_pseudo[name] } end |
.process_frames(data) ⇒ Hash
Advances HTTP/2 frame parsing from the connection buffer, returning updated connection state along with any outgoing protocol frames and completed stream requests.
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 |
# File 'lib/raptor/http2.rb', line 336 def self.process_frames(data) parser = Http2Parser.new buffer = data[:buffer] hpack_table = data[:hpack_table] || [] streams = data[:http2_streams] ? data[:http2_streams].dup : {} outgoing_frames = [] completed_requests = [] window_updates = [] peer_initial_window_size = nil connection_window = data[:http2_window] || DEFAULT_WINDOW_SIZE preface_received = data[:http2_preface_received] || false last_client_stream_id = data[:http2_last_client_stream_id] || 0 pending_headers = data[:http2_pending_headers] goaway_error = nil unless preface_received if buffer.bytesize >= 24 && buffer.byteslice(0, 24) == Http2Parser.connection_preface buffer = buffer.byteslice(24..-1) || "" preface_received = true else return build_result(data, buffer, hpack_table, streams, outgoing_frames, completed_requests, window_updates, peer_initial_window_size, connection_window, preface_received, last_client_stream_id, pending_headers, false) end end loop do parsed = parser.parse_frame(buffer) break unless parsed frame, consumed = parsed buffer = buffer.byteslice(consumed..-1) || "" if pending_headers && frame[:type] != :continuation goaway_error = ERROR_PROTOCOL_ERROR break end case frame[:type] when :settings if frame[:flags].nobits?(FLAG_ACK) parsed_settings = parser.parse_settings(frame[:payload]) peer_initial_window_size = parsed_settings[:initial_window_size] if parsed_settings.key?(:initial_window_size) outgoing_frames << parser.build_frame(:settings, FLAG_ACK, 0, nil) end when :headers stream_id = frame[:stream_id] header_payload = frame[:payload] unless streams.key?(stream_id) if stream_id.even? || stream_id <= last_client_stream_id goaway_error = ERROR_PROTOCOL_ERROR break end last_client_stream_id = stream_id end if frame[:flags].anybits?(FLAG_PRIORITY) header_payload = header_payload.byteslice(5..-1) || "" end end_stream = frame[:flags].anybits?(FLAG_END_STREAM) if frame[:flags].anybits?(FLAG_END_HEADERS) decoded_headers, hpack_table = parser.parse_headers(header_payload, hpack_table) if invalid_pseudo_headers?(decoded_headers) streams.delete(stream_id) outgoing_frames << parser.build_frame(:rst_stream, 0, stream_id, [ERROR_PROTOCOL_ERROR].pack("N")) else streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, end_stream) end else pending_headers = { stream_id: stream_id, buffer: header_payload, end_stream: end_stream } end when :continuation if !pending_headers || frame[:stream_id] != pending_headers[:stream_id] goaway_error = ERROR_PROTOCOL_ERROR break end pending_headers = pending_headers.merge(buffer: pending_headers[:buffer] + frame[:payload]) if frame[:flags].anybits?(FLAG_END_HEADERS) stream_id = pending_headers[:stream_id] decoded_headers, hpack_table = parser.parse_headers(pending_headers[:buffer], hpack_table) if invalid_pseudo_headers?(decoded_headers) streams.delete(stream_id) outgoing_frames << parser.build_frame(:rst_stream, 0, stream_id, [ERROR_PROTOCOL_ERROR].pack("N")) else streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, pending_headers[:end_stream]) end pending_headers = nil end when :data stream_id = frame[:stream_id] unless streams.key?(stream_id) goaway_error = ERROR_PROTOCOL_ERROR break end stream = streams[stream_id] existing_body = stream[:body] || "" stream = stream.merge(body: existing_body + frame[:payload]) if frame[:payload].bytesize.positive? connection_window -= frame[:payload].bytesize if connection_window < DEFAULT_WINDOW_SIZE / 2 increment = DEFAULT_WINDOW_SIZE - connection_window wu_payload = [increment].pack("N") outgoing_frames << parser.build_frame(:window_update, 0, 0, wu_payload) outgoing_frames << parser.build_frame(:window_update, 0, stream_id, wu_payload) connection_window += increment end end if frame[:flags].anybits?(FLAG_END_STREAM) stream_headers = stream[:headers] || [] completed_requests << { stream_id: stream_id, headers: stream_headers, body: stream[:body] } streams.delete(stream_id) else streams[stream_id] = stream end when :window_update increment = parser.parse_window_update(frame[:payload]) window_updates << [frame[:stream_id], increment] when :ping if frame[:flags].nobits?(FLAG_ACK) outgoing_frames << parser.build_frame(:ping, FLAG_ACK, 0, frame[:payload]) end when :goaway break when :rst_stream streams.delete(frame[:stream_id]) end end if goaway_error goaway_payload = [last_client_stream_id, goaway_error].pack("NN") outgoing_frames << parser.build_frame(:goaway, 0, 0, goaway_payload) end build_result(data, buffer, hpack_table, streams, outgoing_frames, completed_requests, window_updates, peer_initial_window_size, connection_window, preface_received, last_client_stream_id, pending_headers, !!goaway_error) end |
Instance Method Details
#apply_flow_control_updates(flow_control, result) ⇒ void
This method returns an undefined value.
Applies inbound flow-control updates from a parsed result to the
connection's FlowControl.
679 680 681 682 683 684 685 686 687 688 689 690 691 |
# File 'lib/raptor/http2.rb', line 679 def apply_flow_control_updates(flow_control, result) result[:window_updates]&.each do |stream_id, increment| if stream_id.zero? flow_control.add_connection_window(increment) else flow_control.add_stream_window(stream_id, increment) end end if (new_size = result[:peer_initial_window_size]) flow_control.set_initial_stream_window(new_size) end end |
#build_rack_env(headers, body, remote_addr:) ⇒ Hash
Builds a Rack environment hash from HTTP/2 headers and body.
841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 |
# File 'lib/raptor/http2.rb', line 841 def build_rack_env(headers, body, remote_addr:) env = {} headers.each do |name, value| if name.start_with?(":") case name when ":method" then env[Rack::REQUEST_METHOD] = value when ":path" path, query = value.split("?", 2) env[Rack::PATH_INFO] = path env[Rack::QUERY_STRING] = query || "" when ":scheme" then env[Rack::RACK_URL_SCHEME] = value when ":authority" then env[Rack::HTTP_HOST] = value end elsif name == "content-type" env[Http::CONTENT_TYPE] = value elsif name == "content-length" env[Http::CONTENT_LENGTH] = value else rack_key = "HTTP_#{name.upcase.tr("-", "_")}" env[rack_key] = value end end env[Rack::SERVER_PROTOCOL] = SERVER_PROTOCOL env[Rack::RACK_VERSION] = Rack::VERSION env[Rack::RACK_INPUT] = StringIO.new(body).set_encoding(Encoding::ASCII_8BIT) env[Rack::RACK_ERRORS] = $stderr env[Rack::RACK_RESPONSE_FINISHED] = [] env[Rack::RACK_IS_HIJACK] = false env[Rack::SCRIPT_NAME] = "" unless env.key?(Rack::SCRIPT_NAME) env[Rack::PATH_INFO] = "" unless env.key?(Rack::PATH_INFO) env[Rack::QUERY_STRING] = "" unless env.key?(Rack::QUERY_STRING) if body.bytesize.positive? && !env.key?(Http::CONTENT_LENGTH) env[Http::CONTENT_LENGTH] = body.bytesize.to_s end env[Http::REMOTE_ADDR] = remote_addr env[Http::SERVER_SOFTWARE] = Http::SERVER_SOFTWARE_VALUE env[Http::HTTP_VERSION] = SERVER_PROTOCOL populate_server_name_and_port(env) env end |
#create_writer ⇒ Writer
Creates a per-connection Writer configured with the handler's write timeout.
312 313 314 |
# File 'lib/raptor/http2.rb', line 312 def create_writer Writer.new(write_timeout: @write_timeout) end |
#dispatch_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:) ⇒ void
This method returns an undefined value.
Dispatches a completed stream request to the Rack app and writes the response back as HTTP/2 frames.
732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 |
# File 'lib/raptor/http2.rb', line 732 def dispatch_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:) env = build_rack_env(headers, body, remote_addr: remote_addr) status, response_headers, response_body = @app.call(env) response_size = write_http2_response(socket, writer, flow_control, stream_id, status, response_headers, response_body) write_access_log(env, status, response_size, remote_addr) if @access_log_io rescue => error write_http2_error_response(socket, writer, stream_id) if @on_error @on_error.call(env, error) rescue nil else raise end ensure response_body.close if response_body.respond_to?(:close) flow_control.discard_stream(stream_id) if flow_control end |
#eager_accept(socket, id, reactor, thread_pool, remote_addr, url_scheme) ⇒ void
This method returns an undefined value.
Sends the server SETTINGS frame on a freshly negotiated HTTP/2 connection, then eagerly reads and parses the first client frame batch, dispatching completed streams directly to the thread pool. Falls back to the reactor when no initial data is ready.
577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 |
# File 'lib/raptor/http2.rb', line 577 def eager_accept(socket, id, reactor, thread_pool, remote_addr, url_scheme) writer = create_writer flow_control = FlowControl.new initial_state = { id: id, protocol: :http2, remote_addr: remote_addr, url_scheme: url_scheme } reactor.attach_http2(id: id, socket: socket, state: initial_state, writer: writer, flow_control: flow_control) socket.write(@initial_settings_frame) rescue nil buffer = begin socket.read_nonblock(EAGER_READ_BUFFER_SIZE) rescue IO::WaitReadable reactor.watch(id) return rescue EOFError, IOError reactor.close_connection(id) return end while socket.pending.positive? buffer << socket.read_nonblock(socket.pending) end result = Raptor::Http2.process_frames(initial_state.merge(buffer: buffer)) handle_parsed_request(result, reactor, thread_pool) rescue => error Log.rescued_error(error) reactor.close_connection(id) end |
#eager_read_next_batch(socket) ⇒ String?
Reads the next frame batch from socket within a short window, or
returns nil if nothing arrives in time.
700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 |
# File 'lib/raptor/http2.rb', line 700 def eager_read_next_batch(socket) return unless socket.wait_readable(EAGER_READ_TIMEOUT) data = begin socket.read_nonblock(EAGER_READ_BUFFER_SIZE) rescue IO::WaitReadable, EOFError, IOError return end buffer = String.new buffer << data while socket.pending.positive? buffer << socket.read_nonblock(socket.pending) end buffer end |
#handle_parsed_request(result, reactor, thread_pool) ⇒ void
This method returns an undefined value.
Handles a parsed HTTP/2 result. Writes outgoing frames, dispatches completed stream requests to the thread pool, and eagerly consumes further buffered frame batches before returning control to the reactor.
623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 |
# File 'lib/raptor/http2.rb', line 623 def handle_parsed_request(result, reactor, thread_pool) socket = reactor.socket_for(result[:id]) return unless socket writer = reactor.writer_for(result[:id]) flow_control = reactor.flow_control_for(result[:id]) rounds = 0 loop do if flow_control && (result[:window_updates] || result[:peer_initial_window_size]) apply_flow_control_updates(flow_control, result) end writer.write_frames(socket, result[:outgoing_frames]) if result[:close_connection] reactor.close_connection(result[:id]) return end result[:completed_requests]&.each do |request| stream_id = request[:stream_id] remote_addr = result[:remote_addr] || Server::DEFAULT_REMOTE_ADDR thread_pool << proc do dispatch_stream_request( socket, writer, flow_control, stream_id, request[:headers], request[:body], remote_addr: remote_addr ) end end rounds += 1 break if rounds >= EAGER_MAX_ROUNDS break if thread_pool.queue_size >= thread_pool.size next_batch = eager_read_next_batch(socket) break unless next_batch result = Raptor::Http2.process_frames(result.merge(buffer: result[:buffer] + next_batch)) end reactor.update_http2_state(result) end |
#parser_worker ⇒ Proc
Returns a Ractor-safe proc that parses HTTP/2 frames from the state hash's buffered bytes.
322 323 324 325 326 |
# File 'lib/raptor/http2.rb', line 322 def parser_worker proc do |data| Raptor::Http2.process_frames(data) end end |
#populate_server_name_and_port(env) ⇒ void
This method returns an undefined value.
Populates SERVER_NAME and SERVER_PORT from the HTTP_HOST header.
895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 |
# File 'lib/raptor/http2.rb', line 895 def populate_server_name_and_port(env) http_host = env[Rack::HTTP_HOST] if http_host if http_host.start_with?("[") host = http_host[/\A\[([^\]]+)\]/, 1] port = http_host[/\]:(\d+)\z/, 1] else host, port = http_host.split(":", 2) end env[Rack::SERVER_NAME] ||= host env[Rack::SERVER_PORT] ||= port || @server_port.to_s else env[Rack::SERVER_NAME] ||= Server::DEFAULT_SERVER_NAME env[Rack::SERVER_PORT] ||= @server_port.to_s end end |
#write_access_log(env, status, size, remote_addr) ⇒ void
This method returns an undefined value.
Instance-level wrapper around Raptor::Http.write_access_log that routes to
the configured @access_log_io.
829 830 831 |
# File 'lib/raptor/http2.rb', line 829 def write_access_log(env, status, size, remote_addr) Http.write_access_log(@access_log_io, env, status, size, remote_addr) end |
#write_http2_error_response(socket, writer, stream_id) ⇒ void
This method returns an undefined value.
Writes a 500 error response as HTTP/2 frames.
809 810 811 812 813 814 815 816 817 |
# File 'lib/raptor/http2.rb', line 809 def write_http2_error_response(socket, writer, stream_id) parser = Http2Parser.new encoded = parser.encode_headers([[":status", "500"]]) writer.write_frames( socket, [parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, stream_id, encoded)] ) end |
#write_http2_response(socket, writer, flow_control, stream_id, status, headers, body) ⇒ String
Writes a Rack response as HTTP/2 frames to the socket, partitioning
DATA frames through flow_control to fit within the peer's windows.
764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 |
# File 'lib/raptor/http2.rb', line 764 def write_http2_response(socket, writer, flow_control, stream_id, status, headers, body) parser = Http2Parser.new encoded_headers = parser.encode_response_headers(status, headers) body_chunks = [] body_bytes = 0 body.each do |chunk| next if chunk.empty? body_chunks << chunk body_bytes += chunk.bytesize end if body_chunks.empty? writer.write_frames(socket, [parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, stream_id, encoded_headers)]) return "0" end frames = [parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded_headers)] last_chunk_index = body_chunks.size - 1 body_chunks.each_with_index do |chunk, chunk_index| offset = 0 while offset < chunk.bytesize remaining = chunk.bytesize - offset last_frame = chunk_index == last_chunk_index && remaining <= MAX_FRAME_SIZE granted = flow_control.acquire(stream_id, remaining, end_stream: last_frame) slice = offset.zero? && granted == chunk.bytesize ? chunk : chunk.byteslice(offset, granted) offset += granted end_stream = chunk_index == last_chunk_index && offset == chunk.bytesize frames << parser.build_frame(:data, end_stream ? FLAG_END_STREAM : 0, stream_id, slice) end end writer.write_frames(socket, frames) body_bytes.to_s end |