raptor 0.22.3 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -0
- data/Dockerfile +10 -2
- data/README.md +183 -61
- data/docs/assets/profile_picture.png +0 -0
- data/docs/assets/startup_output.svg +37 -0
- data/docs/raptor-vs-puma.md +104 -424
- data/docs/talk.md +1388 -0
- data/ext/raptor_bpf/reuseport_select.bpf.c +18 -9
- data/ext/raptor_http/extconf.rb +2 -0
- data/ext/raptor_http/raptor_http.c +165 -234
- data/ext/raptor_http2/raptor_http2.c +272 -223
- data/ext/raptor_native/raptor_native.c +38 -20
- data/lib/rackup/handler/raptor.rb +14 -12
- data/lib/raptor/binder.rb +12 -12
- data/lib/raptor/cli.rb +19 -18
- data/lib/raptor/cluster.rb +120 -74
- data/lib/raptor/control_server.rb +5 -2
- data/lib/raptor/detached_body.rb +6 -2
- data/lib/raptor/http.rb +7 -6
- data/lib/raptor/http1.rb +281 -234
- data/lib/raptor/http2.rb +356 -276
- data/lib/raptor/plugin.rb +98 -0
- data/lib/raptor/reactor.rb +138 -148
- data/lib/raptor/reuseport_bpf.rb +62 -38
- data/lib/raptor/server.rb +45 -34
- data/lib/raptor/stats.rb +15 -3
- data/lib/raptor/thread_locals.rb +4 -3
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/rackup/handler/raptor.rbs +2 -1
- data/sig/generated/raptor/binder.rbs +9 -9
- data/sig/generated/raptor/cli.rbs +6 -6
- data/sig/generated/raptor/cluster.rbs +18 -5
- data/sig/generated/raptor/control_server.rbs +2 -1
- data/sig/generated/raptor/detached_body.rbs +5 -1
- data/sig/generated/raptor/http.rbs +3 -5
- data/sig/generated/raptor/http1.rbs +119 -102
- data/sig/generated/raptor/http2.rbs +204 -190
- data/sig/generated/raptor/plugin.rbs +52 -0
- data/sig/generated/raptor/reactor.rbs +107 -118
- data/sig/generated/raptor/reuseport_bpf.rbs +54 -17
- data/sig/generated/raptor/server.rbs +26 -26
- data/sig/generated/raptor/stats.rbs +3 -2
- data/sig/generated/raptor/thread_locals.rbs +4 -3
- metadata +6 -2
- data/docs/brisrails-talk.md +0 -2398
data/lib/raptor/http2.rb
CHANGED
|
@@ -10,14 +10,44 @@ require "rack"
|
|
|
10
10
|
|
|
11
11
|
require_relative "detached_body"
|
|
12
12
|
require_relative "http"
|
|
13
|
+
require_relative "log"
|
|
13
14
|
require_relative "raptor_http2"
|
|
15
|
+
require_relative "server"
|
|
14
16
|
require_relative "thread_locals"
|
|
15
17
|
|
|
16
18
|
module Raptor
|
|
17
19
|
# Handles HTTP/2 request processing and Rack application integration.
|
|
18
20
|
#
|
|
19
21
|
class Http2
|
|
22
|
+
EAGER_READ_BUFFER_SIZE = 64 * 1024
|
|
23
|
+
EAGER_MAX_ROUNDS = 8
|
|
24
|
+
|
|
25
|
+
FLAG_END_STREAM = 0x1
|
|
26
|
+
FLAG_ACK = 0x1
|
|
27
|
+
FLAG_END_HEADERS = 0x4
|
|
28
|
+
FLAG_PADDED = 0x8
|
|
29
|
+
FLAG_PRIORITY = 0x20
|
|
30
|
+
|
|
31
|
+
ERROR_NO_ERROR = 0x0
|
|
32
|
+
ERROR_PROTOCOL_ERROR = 0x1
|
|
33
|
+
ERROR_INTERNAL_ERROR = 0x2
|
|
34
|
+
ERROR_FRAME_SIZE_ERROR = 0x6
|
|
35
|
+
ERROR_REFUSED_STREAM = 0x7
|
|
36
|
+
ERROR_COMPRESSION_ERROR = 0x9
|
|
37
|
+
|
|
38
|
+
DEFAULT_WINDOW_SIZE = 65_535
|
|
39
|
+
MAX_FRAME_SIZE = 16_384
|
|
40
|
+
|
|
20
41
|
RESPONSE_TRAILERS = "raptor.response_trailers"
|
|
42
|
+
SERVER_PROTOCOL = "HTTP/2"
|
|
43
|
+
|
|
44
|
+
REQUEST_PSEUDO_HEADERS = [":method", ":scheme", ":path", ":authority"].freeze
|
|
45
|
+
REQUIRED_REQUEST_PSEUDO_HEADERS = [":method", ":scheme", ":path"].freeze
|
|
46
|
+
|
|
47
|
+
class StreamClosedError < StandardError
|
|
48
|
+
# @rbs () -> String
|
|
49
|
+
def message = "HTTP/2 stream closed"
|
|
50
|
+
end
|
|
21
51
|
|
|
22
52
|
# Queues concurrent frame writes while the reactor remains the sole
|
|
23
53
|
# owner of the connection socket.
|
|
@@ -54,11 +84,11 @@ module Raptor
|
|
|
54
84
|
# Queues frames for the reactor to write. Before attachment, writes
|
|
55
85
|
# directly so the writer remains usable during connection setup.
|
|
56
86
|
#
|
|
57
|
-
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
87
|
+
# @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
|
|
58
88
|
# @param frames [Array<String>, nil] frame bytes to write in order
|
|
59
89
|
# @return [void]
|
|
60
90
|
#
|
|
61
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket, Array[String]? frames) -> void
|
|
91
|
+
# @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Array[String]? frames) -> void
|
|
62
92
|
def write_frames(socket, frames)
|
|
63
93
|
return if !frames || frames.empty?
|
|
64
94
|
|
|
@@ -82,11 +112,6 @@ module Raptor
|
|
|
82
112
|
end
|
|
83
113
|
end
|
|
84
114
|
|
|
85
|
-
class StreamClosedError < StandardError
|
|
86
|
-
# @rbs () -> String
|
|
87
|
-
def message = "HTTP/2 stream closed"
|
|
88
|
-
end
|
|
89
|
-
|
|
90
115
|
# Coordinates outbound flow control and stream cancellation without
|
|
91
116
|
# polling or mutexes.
|
|
92
117
|
#
|
|
@@ -112,21 +137,22 @@ module Raptor
|
|
|
112
137
|
|
|
113
138
|
# Reserves outbound capacity on the given stream, waiting until at
|
|
114
139
|
# least one byte is available on both the connection and stream
|
|
115
|
-
# windows.
|
|
140
|
+
# windows.
|
|
116
141
|
#
|
|
117
142
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
118
143
|
# @param max_bytes [Integer] the largest size the caller would like to send
|
|
119
144
|
# @return [Integer] the number of bytes the caller may now send
|
|
145
|
+
# @raise [StreamClosedError] if the stream or connection has closed
|
|
120
146
|
#
|
|
121
147
|
# @rbs (Integer stream_id, Integer max_bytes) -> Integer
|
|
122
148
|
def acquire(stream_id, max_bytes)
|
|
123
|
-
|
|
124
|
-
capped = max_bytes < MAX_FRAME_SIZE ? max_bytes : MAX_FRAME_SIZE
|
|
149
|
+
capped = [max_bytes, MAX_FRAME_SIZE].min
|
|
125
150
|
|
|
126
151
|
@available.wait do
|
|
127
152
|
check(stream_id)
|
|
153
|
+
initial = @initial_stream_window.value
|
|
128
154
|
stream_window = @stream_windows.value[stream_id] || initial
|
|
129
|
-
capped_full = capped
|
|
155
|
+
capped_full = [capped, stream_window].min
|
|
130
156
|
granted = capped_full.positive? ? reserve_connection(capped_full) : 0
|
|
131
157
|
|
|
132
158
|
if granted.positive?
|
|
@@ -144,6 +170,7 @@ module Raptor
|
|
|
144
170
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
145
171
|
# @param max_bytes [Integer] the largest size the caller would like to send
|
|
146
172
|
# @return [Integer] available bytes, or 0 when flow control is exhausted
|
|
173
|
+
# @raise [StreamClosedError] if the stream or connection has closed
|
|
147
174
|
#
|
|
148
175
|
# @rbs (Integer stream_id, Integer max_bytes) -> Integer
|
|
149
176
|
def try_acquire(stream_id, max_bytes)
|
|
@@ -191,7 +218,7 @@ module Raptor
|
|
|
191
218
|
end
|
|
192
219
|
|
|
193
220
|
# Updates the peer's `SETTINGS_INITIAL_WINDOW_SIZE`. Shifts every
|
|
194
|
-
# existing stream window by the delta as required by RFC
|
|
221
|
+
# existing stream window by the delta as required by RFC 9113 section 6.9.2.
|
|
195
222
|
#
|
|
196
223
|
# @param new_size [Integer] the peer's new initial window size
|
|
197
224
|
# @return [void]
|
|
@@ -267,18 +294,16 @@ module Raptor
|
|
|
267
294
|
|
|
268
295
|
private
|
|
269
296
|
|
|
270
|
-
# Reserves up to `capped` bytes from the connection window
|
|
271
|
-
# returning the number of bytes granted (0 when the window is
|
|
272
|
-
# exhausted).
|
|
297
|
+
# Reserves up to `capped` bytes from the connection window.
|
|
273
298
|
#
|
|
274
299
|
# @param capped [Integer] the largest reservation the caller will accept
|
|
275
|
-
# @return [Integer]
|
|
300
|
+
# @return [Integer] bytes granted, or 0 when the window is exhausted
|
|
276
301
|
#
|
|
277
302
|
# @rbs (Integer capped) -> Integer
|
|
278
303
|
def reserve_connection(capped)
|
|
279
304
|
granted = 0
|
|
280
305
|
@connection_window.swap do |window|
|
|
281
|
-
granted = window
|
|
306
|
+
granted = [window, capped].min
|
|
282
307
|
granted.positive? ? window - granted : window
|
|
283
308
|
end
|
|
284
309
|
granted
|
|
@@ -288,7 +313,7 @@ module Raptor
|
|
|
288
313
|
# Adapts one HTTP/2 response stream to Rack's streaming body interface.
|
|
289
314
|
#
|
|
290
315
|
class ResponseStream
|
|
291
|
-
# @rbs @socket: OpenSSL::SSL::SSLSocket
|
|
316
|
+
# @rbs @socket: TCPSocket | OpenSSL::SSL::SSLSocket
|
|
292
317
|
# @rbs @writer: Writer
|
|
293
318
|
# @rbs @flow_control: FlowControl
|
|
294
319
|
# @rbs @stream_id: Integer
|
|
@@ -304,14 +329,14 @@ module Raptor
|
|
|
304
329
|
|
|
305
330
|
# Creates a response stream for a Rack streaming body.
|
|
306
331
|
#
|
|
307
|
-
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
332
|
+
# @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
|
|
308
333
|
# @param writer [Writer] frame writer for the connection
|
|
309
|
-
# @param flow_control [FlowControl] outbound flow controller
|
|
334
|
+
# @param flow_control [FlowControl] per-connection outbound flow controller
|
|
310
335
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
311
336
|
# @param trailers [Hash] trailing response headers populated by the Rack application
|
|
312
337
|
# @return [void]
|
|
313
338
|
#
|
|
314
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Hash[String, String | Array[String]] trailers) -> void
|
|
339
|
+
# @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Hash[String, String | Array[String]] trailers) -> void
|
|
315
340
|
def initialize(socket, writer, flow_control, stream_id, trailers)
|
|
316
341
|
@socket = socket
|
|
317
342
|
@writer = writer
|
|
@@ -323,9 +348,9 @@ module Raptor
|
|
|
323
348
|
@closed = false
|
|
324
349
|
end
|
|
325
350
|
|
|
326
|
-
#
|
|
351
|
+
# Rejects reads; streaming responses are write-only.
|
|
327
352
|
#
|
|
328
|
-
# @raise [IOError]
|
|
353
|
+
# @raise [IOError] always
|
|
329
354
|
#
|
|
330
355
|
# @rbs (*untyped) -> bot
|
|
331
356
|
def read(*)
|
|
@@ -336,6 +361,9 @@ module Raptor
|
|
|
336
361
|
#
|
|
337
362
|
# @param chunk [String] response body bytes
|
|
338
363
|
# @return [Integer] number of bytes written
|
|
364
|
+
# @raise [IOError] if the stream has already closed
|
|
365
|
+
# @raise [TypeError] if the chunk is not a String
|
|
366
|
+
# @raise [StreamClosedError] if the stream or connection has closed
|
|
339
367
|
#
|
|
340
368
|
# @rbs (String chunk) -> Integer
|
|
341
369
|
def write(chunk)
|
|
@@ -365,7 +393,8 @@ module Raptor
|
|
|
365
393
|
self
|
|
366
394
|
end
|
|
367
395
|
|
|
368
|
-
# Flushes the stream.
|
|
396
|
+
# Flushes the stream. Frames are already handed to the writer as they
|
|
397
|
+
# are produced.
|
|
369
398
|
#
|
|
370
399
|
# @return [ResponseStream]
|
|
371
400
|
#
|
|
@@ -377,6 +406,7 @@ module Raptor
|
|
|
377
406
|
# Finishes the stream.
|
|
378
407
|
#
|
|
379
408
|
# @return [void]
|
|
409
|
+
# @raise [StreamClosedError] if the stream or connection has closed
|
|
380
410
|
#
|
|
381
411
|
# @rbs () -> void
|
|
382
412
|
def close
|
|
@@ -395,7 +425,7 @@ module Raptor
|
|
|
395
425
|
|
|
396
426
|
# Closes the unsupported read side of the stream.
|
|
397
427
|
#
|
|
398
|
-
# @raise [IOError]
|
|
428
|
+
# @raise [IOError] always
|
|
399
429
|
#
|
|
400
430
|
# @rbs () -> bot
|
|
401
431
|
def close_read
|
|
@@ -421,148 +451,11 @@ module Raptor
|
|
|
421
451
|
end
|
|
422
452
|
end
|
|
423
453
|
|
|
424
|
-
EAGER_READ_BUFFER_SIZE = 64 * 1024
|
|
425
|
-
EAGER_MAX_ROUNDS = 8
|
|
426
|
-
|
|
427
|
-
FLAG_END_STREAM = 0x1
|
|
428
|
-
FLAG_END_HEADERS = 0x4
|
|
429
|
-
FLAG_ACK = 0x1
|
|
430
|
-
FLAG_PRIORITY = 0x20
|
|
431
|
-
|
|
432
|
-
ERROR_NO_ERROR = 0x0
|
|
433
|
-
ERROR_PROTOCOL_ERROR = 0x1
|
|
434
|
-
ERROR_INTERNAL_ERROR = 0x2
|
|
435
|
-
ERROR_REFUSED_STREAM = 0x7
|
|
436
|
-
|
|
437
|
-
DEFAULT_WINDOW_SIZE = 65_535
|
|
438
|
-
MAX_FRAME_SIZE = 16_384
|
|
439
|
-
|
|
440
|
-
SERVER_PROTOCOL = "HTTP/2"
|
|
441
|
-
|
|
442
|
-
REQUEST_PSEUDO_HEADERS = [":method", ":scheme", ":path", ":authority"].freeze
|
|
443
|
-
REQUIRED_REQUEST_PSEUDO_HEADERS = [":method", ":scheme", ":path"].freeze
|
|
444
|
-
|
|
445
|
-
# Returns true when a decoded header block violates the HTTP/2 pseudo-header
|
|
446
|
-
# rules from RFC 9113 section 8.3: an unknown pseudo-header, a duplicate
|
|
447
|
-
# pseudo-header, a pseudo-header appearing after any regular header, or a
|
|
448
|
-
# required pseudo-header (`:method`, `:scheme`, `:path`) missing on
|
|
449
|
-
# non-`CONNECT` requests.
|
|
450
|
-
#
|
|
451
|
-
# @param headers [Array<Array(String, String)>] decoded header pairs
|
|
452
|
-
# @return [Boolean]
|
|
453
|
-
#
|
|
454
|
-
# @rbs (Array[[String, String]] headers) -> bool
|
|
455
|
-
def self.invalid_pseudo_headers?(headers)
|
|
456
|
-
seen_pseudo = {}
|
|
457
|
-
seen_regular = false
|
|
458
|
-
|
|
459
|
-
headers.each do |name, _value|
|
|
460
|
-
if name.start_with?(":")
|
|
461
|
-
return true if seen_regular
|
|
462
|
-
return true unless REQUEST_PSEUDO_HEADERS.include?(name)
|
|
463
|
-
return true if seen_pseudo[name]
|
|
464
|
-
seen_pseudo[name] = true
|
|
465
|
-
else
|
|
466
|
-
seen_regular = true
|
|
467
|
-
end
|
|
468
|
-
end
|
|
469
|
-
|
|
470
|
-
return false if seen_pseudo[":method"] && headers.assoc(":method")&.last == "CONNECT"
|
|
471
|
-
|
|
472
|
-
REQUIRED_REQUEST_PSEUDO_HEADERS.any? { |name| !seen_pseudo[name] }
|
|
473
|
-
end
|
|
474
|
-
|
|
475
|
-
# @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
|
|
476
|
-
# @rbs @server_port: Integer
|
|
477
|
-
# @rbs @write_timeout: Integer
|
|
478
|
-
# @rbs @access_log_io: IO?
|
|
479
|
-
# @rbs @clean_thread_locals: bool
|
|
480
|
-
# @rbs @clean_fiber_locals: bool
|
|
481
|
-
# @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
482
|
-
# @rbs @initial_settings_frame: String
|
|
483
|
-
# @rbs @running: AtomicBoolean
|
|
484
|
-
|
|
485
|
-
# Returns the initial server SETTINGS frame to send on every new
|
|
486
|
-
# HTTP/2 connection.
|
|
487
|
-
#
|
|
488
|
-
# @return [String]
|
|
489
|
-
attr_reader :initial_settings_frame
|
|
490
|
-
|
|
491
|
-
# Creates a new Http2 handler.
|
|
492
|
-
#
|
|
493
|
-
# @param app [#call] the Rack application to dispatch requests to
|
|
494
|
-
# @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
|
|
495
|
-
# @param connection_options [Hash] per-connection settings shared across protocols
|
|
496
|
-
# @option connection_options [Integer] :write_timeout per-write socket timeout in seconds
|
|
497
|
-
# @param http2_options [Hash] HTTP/2-specific settings
|
|
498
|
-
# @option http2_options [Integer] :max_concurrent_streams maximum HTTP/2 concurrent streams per connection
|
|
499
|
-
# @param access_log_io [IO, nil] IO to write Common Log Format access entries to, or nil to disable
|
|
500
|
-
# @param clean_thread_locals [Boolean] whether to clear application thread locals after each request
|
|
501
|
-
# @param clean_fiber_locals [Boolean] whether to process each request in a fresh Fiber
|
|
502
|
-
# @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
|
|
503
|
-
# @return [void]
|
|
504
|
-
#
|
|
505
|
-
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?connection_options: Hash[Symbol, untyped], ?http2_options: Hash[Symbol, untyped], ?access_log_io: IO?, ?clean_thread_locals: bool, ?clean_fiber_locals: bool, ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
|
|
506
|
-
def initialize(app, server_port, connection_options: {}, http2_options: {}, access_log_io: nil, clean_thread_locals: true, clean_fiber_locals: false, on_error: nil)
|
|
507
|
-
@app = app
|
|
508
|
-
@server_port = server_port
|
|
509
|
-
@write_timeout = connection_options[:write_timeout] || Http::WRITE_TIMEOUT
|
|
510
|
-
@access_log_io = access_log_io
|
|
511
|
-
@clean_thread_locals = clean_thread_locals
|
|
512
|
-
@clean_fiber_locals = clean_fiber_locals
|
|
513
|
-
@on_error = on_error
|
|
514
|
-
@running = AtomicBoolean.new(true)
|
|
515
|
-
|
|
516
|
-
parser = Http2Parser.new
|
|
517
|
-
settings_payload = parser.build_settings(
|
|
518
|
-
max_concurrent_streams: http2_options[:max_concurrent_streams],
|
|
519
|
-
initial_window_size: DEFAULT_WINDOW_SIZE
|
|
520
|
-
)
|
|
521
|
-
@initial_settings_frame = parser.build_frame(:settings, 0, 0, settings_payload).freeze
|
|
522
|
-
end
|
|
523
|
-
|
|
524
|
-
# Creates a per-connection {Writer} configured with the handler's write timeout.
|
|
525
|
-
#
|
|
526
|
-
# @return [Writer] a new per-connection frame writer
|
|
527
|
-
#
|
|
528
|
-
# @rbs () -> Writer
|
|
529
|
-
def create_writer
|
|
530
|
-
Writer.new(write_timeout: @write_timeout)
|
|
531
|
-
end
|
|
532
|
-
|
|
533
|
-
# Stops new stream dispatch and asks clients to finish streams already
|
|
534
|
-
# handed to the application.
|
|
535
|
-
#
|
|
536
|
-
# @param reactor [Reactor] the reactor managing HTTP/2 connections
|
|
537
|
-
# @return [void]
|
|
538
|
-
#
|
|
539
|
-
# @rbs (Reactor reactor) -> void
|
|
540
|
-
def shutdown(reactor)
|
|
541
|
-
@running.make_false
|
|
542
|
-
parser = Http2Parser.new
|
|
543
|
-
reactor.drain_http2 do |stream_id|
|
|
544
|
-
payload = [stream_id, ERROR_NO_ERROR].pack("NN")
|
|
545
|
-
parser.build_frame(:goaway, 0, 0, payload)
|
|
546
|
-
end
|
|
547
|
-
end
|
|
548
|
-
|
|
549
|
-
# Returns a Ractor-safe proc that parses HTTP/2 frames from the
|
|
550
|
-
# state hash's buffered bytes.
|
|
551
|
-
#
|
|
552
|
-
# @return [Proc]
|
|
553
|
-
#
|
|
554
|
-
# @rbs () -> ^(Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
555
|
-
def parser_worker
|
|
556
|
-
proc do |data|
|
|
557
|
-
Raptor::Http2.process_frames(data)
|
|
558
|
-
end
|
|
559
|
-
end
|
|
560
|
-
|
|
561
454
|
# Advances HTTP/2 frame parsing from the connection buffer, returning
|
|
562
455
|
# updated connection state along with any outgoing protocol frames and
|
|
563
456
|
# completed stream requests.
|
|
564
457
|
#
|
|
565
|
-
# @param data [Hash] the connection state including buffer and
|
|
458
|
+
# @param data [Hash] the connection state including buffer, HPACK table and body-size limit
|
|
566
459
|
# @return [Hash] updated state with outgoing_frames and completed_requests
|
|
567
460
|
#
|
|
568
461
|
# @rbs (Hash[Symbol, untyped] data) -> Hash[Symbol, untyped]
|
|
@@ -581,6 +474,7 @@ module Raptor
|
|
|
581
474
|
preface_received = data[:http2_preface_received] || false
|
|
582
475
|
last_client_stream_id = data[:http2_last_client_stream_id] || 0
|
|
583
476
|
pending_headers = data[:http2_pending_headers]
|
|
477
|
+
max_body_size = data[:max_body_size]
|
|
584
478
|
goaway_error = nil
|
|
585
479
|
|
|
586
480
|
unless preface_received
|
|
@@ -593,6 +487,12 @@ module Raptor
|
|
|
593
487
|
end
|
|
594
488
|
|
|
595
489
|
loop do
|
|
490
|
+
# Reject oversized frames before waiting for their payload.
|
|
491
|
+
if buffer.bytesize >= 3 && (buffer.getbyte(0) << 16 | buffer.getbyte(1) << 8 | buffer.getbyte(2)) > MAX_FRAME_SIZE
|
|
492
|
+
goaway_error = ERROR_FRAME_SIZE_ERROR
|
|
493
|
+
break
|
|
494
|
+
end
|
|
495
|
+
|
|
596
496
|
parsed = parser.parse_frame(buffer)
|
|
597
497
|
break unless parsed
|
|
598
498
|
|
|
@@ -614,9 +514,14 @@ module Raptor
|
|
|
614
514
|
|
|
615
515
|
when :headers
|
|
616
516
|
stream_id = frame[:stream_id]
|
|
617
|
-
header_payload = frame
|
|
517
|
+
header_payload = unpadded_payload(frame)
|
|
618
518
|
trailers = streams.key?(stream_id)
|
|
619
519
|
|
|
520
|
+
unless header_payload
|
|
521
|
+
goaway_error = ERROR_PROTOCOL_ERROR
|
|
522
|
+
break
|
|
523
|
+
end
|
|
524
|
+
|
|
620
525
|
unless trailers
|
|
621
526
|
if stream_id.even? || stream_id <= last_client_stream_id
|
|
622
527
|
goaway_error = ERROR_PROTOCOL_ERROR
|
|
@@ -648,6 +553,10 @@ module Raptor
|
|
|
648
553
|
end
|
|
649
554
|
|
|
650
555
|
pending_headers = pending_headers.merge(buffer: pending_headers[:buffer] + frame[:payload])
|
|
556
|
+
if pending_headers[:buffer].bytesize > Http2Parser::MAX_HEADER_LIST_SIZE
|
|
557
|
+
goaway_error = ERROR_PROTOCOL_ERROR
|
|
558
|
+
break
|
|
559
|
+
end
|
|
651
560
|
|
|
652
561
|
if frame[:flags].anybits?(FLAG_END_HEADERS)
|
|
653
562
|
stream_id = pending_headers[:stream_id]
|
|
@@ -661,27 +570,47 @@ module Raptor
|
|
|
661
570
|
|
|
662
571
|
when :data
|
|
663
572
|
stream_id = frame[:stream_id]
|
|
573
|
+
chunk = unpadded_payload(frame)
|
|
664
574
|
|
|
665
|
-
|
|
575
|
+
if stream_id.zero? || stream_id > last_client_stream_id || !chunk
|
|
666
576
|
goaway_error = ERROR_PROTOCOL_ERROR
|
|
667
577
|
break
|
|
668
578
|
end
|
|
669
579
|
|
|
580
|
+
# DATA for closed streams still counts toward connection flow control.
|
|
581
|
+
connection_window -= frame[:length]
|
|
582
|
+
if connection_window < DEFAULT_WINDOW_SIZE / 2
|
|
583
|
+
outgoing_frames << parser.build_frame(:window_update, 0, 0, [DEFAULT_WINDOW_SIZE - connection_window].pack("N"))
|
|
584
|
+
connection_window = DEFAULT_WINDOW_SIZE
|
|
585
|
+
end
|
|
586
|
+
|
|
670
587
|
stream = streams[stream_id]
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
588
|
+
next unless stream
|
|
589
|
+
|
|
590
|
+
if stream[:too_large]
|
|
591
|
+
streams.delete(stream_id) if frame[:flags].anybits?(FLAG_END_STREAM)
|
|
592
|
+
next
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
body = stream[:body] || ""
|
|
596
|
+
if max_body_size && body.bytesize + chunk.bytesize > max_body_size
|
|
597
|
+
headers = parser.encode_headers([[":status", "413"], ["content-length", "0"]])
|
|
598
|
+
outgoing_frames << parser.build_frame(:headers, FLAG_END_HEADERS | FLAG_END_STREAM, stream_id, headers)
|
|
599
|
+
if frame[:flags].anybits?(FLAG_END_STREAM)
|
|
600
|
+
streams.delete(stream_id)
|
|
601
|
+
else
|
|
602
|
+
streams[stream_id] = { too_large: true }
|
|
682
603
|
end
|
|
604
|
+
next
|
|
683
605
|
end
|
|
684
606
|
|
|
607
|
+
stream_window_used = (stream[:window_used] || 0) + frame[:length]
|
|
608
|
+
if stream_window_used >= DEFAULT_WINDOW_SIZE / 2 && frame[:flags].nobits?(FLAG_END_STREAM)
|
|
609
|
+
outgoing_frames << parser.build_frame(:window_update, 0, stream_id, [stream_window_used].pack("N"))
|
|
610
|
+
stream_window_used = 0
|
|
611
|
+
end
|
|
612
|
+
stream = stream.merge(body: body + chunk, window_used: stream_window_used)
|
|
613
|
+
|
|
685
614
|
if frame[:flags].anybits?(FLAG_END_STREAM)
|
|
686
615
|
stream_headers = stream[:headers] || []
|
|
687
616
|
completed_requests << {
|
|
@@ -715,6 +644,14 @@ module Raptor
|
|
|
715
644
|
completed_requests.reject! { |request| request[:stream_id] == stream_id }
|
|
716
645
|
cancelled_streams << stream_id
|
|
717
646
|
end
|
|
647
|
+
rescue Http2ParserError
|
|
648
|
+
# A failed header decode can desynchronize HPACK state for the whole connection.
|
|
649
|
+
goaway_error = if frame[:type] == :headers || frame[:type] == :continuation
|
|
650
|
+
ERROR_COMPRESSION_ERROR
|
|
651
|
+
else
|
|
652
|
+
ERROR_FRAME_SIZE_ERROR
|
|
653
|
+
end
|
|
654
|
+
break
|
|
718
655
|
end
|
|
719
656
|
|
|
720
657
|
if goaway_error
|
|
@@ -725,6 +662,50 @@ module Raptor
|
|
|
725
662
|
build_result(data, buffer, hpack_table, streams, outgoing_frames, completed_requests, cancelled_streams, window_updates, ping_acknowledgements, peer_initial_window_size, connection_window, preface_received, last_client_stream_id, pending_headers, !!goaway_error)
|
|
726
663
|
end
|
|
727
664
|
|
|
665
|
+
# Returns a DATA or HEADERS payload without padding, or nil if padding
|
|
666
|
+
# is invalid.
|
|
667
|
+
#
|
|
668
|
+
# @param frame [Hash] the parsed frame
|
|
669
|
+
# @return [String, nil]
|
|
670
|
+
#
|
|
671
|
+
# @rbs (Hash[Symbol, untyped] frame) -> String?
|
|
672
|
+
def self.unpadded_payload(frame)
|
|
673
|
+
payload = frame[:payload]
|
|
674
|
+
return payload if frame[:flags].nobits?(FLAG_PADDED)
|
|
675
|
+
|
|
676
|
+
pad_length = payload.getbyte(0)
|
|
677
|
+
payload.byteslice(1, payload.bytesize - 1 - pad_length) if pad_length && pad_length < payload.bytesize
|
|
678
|
+
end
|
|
679
|
+
private_class_method :unpadded_payload
|
|
680
|
+
|
|
681
|
+
# Returns whether request pseudo-headers violate RFC 9113 section 8.3.
|
|
682
|
+
#
|
|
683
|
+
# @param headers [Array<Array(String, String)>] decoded header pairs
|
|
684
|
+
# @return [Boolean]
|
|
685
|
+
#
|
|
686
|
+
# @rbs (Array[[String, String]] headers) -> bool
|
|
687
|
+
def self.invalid_pseudo_headers?(headers)
|
|
688
|
+
seen_pseudo = {}
|
|
689
|
+
seen_regular = false
|
|
690
|
+
|
|
691
|
+
headers.each do |name, _value|
|
|
692
|
+
if name.start_with?(":")
|
|
693
|
+
return true if seen_regular
|
|
694
|
+
return true unless REQUEST_PSEUDO_HEADERS.include?(name)
|
|
695
|
+
return true if seen_pseudo[name]
|
|
696
|
+
|
|
697
|
+
seen_pseudo[name] = true
|
|
698
|
+
else
|
|
699
|
+
seen_regular = true
|
|
700
|
+
end
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
return false if seen_pseudo[":method"] && headers.assoc(":method")&.last == "CONNECT"
|
|
704
|
+
|
|
705
|
+
REQUIRED_REQUEST_PSEUDO_HEADERS.any? { |name| !seen_pseudo[name] }
|
|
706
|
+
end
|
|
707
|
+
private_class_method :invalid_pseudo_headers?
|
|
708
|
+
|
|
728
709
|
# Applies one complete request header block to its stream, resetting
|
|
729
710
|
# malformed initial headers or trailers and completing valid trailers.
|
|
730
711
|
#
|
|
@@ -732,7 +713,7 @@ module Raptor
|
|
|
732
713
|
# @param completed_requests [Array<Hash>] accumulator of completed stream requests
|
|
733
714
|
# @param outgoing_frames [Array<String>] accumulator of protocol response frames
|
|
734
715
|
# @param parser [Http2Parser] frame parser and encoder
|
|
735
|
-
# @param stream_id [Integer] the stream identifier
|
|
716
|
+
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
736
717
|
# @param decoded_headers [Array<Array(String, String)>] decoded header pairs
|
|
737
718
|
# @param end_stream [Boolean] whether the source frame had END_STREAM set
|
|
738
719
|
# @param trailers [Boolean] whether this is a trailing header block
|
|
@@ -751,11 +732,13 @@ module Raptor
|
|
|
751
732
|
outgoing_frames << parser.build_frame(:rst_stream, 0, stream_id, [ERROR_PROTOCOL_ERROR].pack("N"))
|
|
752
733
|
elsif trailers
|
|
753
734
|
stream = streams.delete(stream_id)
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
735
|
+
unless stream[:too_large]
|
|
736
|
+
completed_requests << {
|
|
737
|
+
stream_id: stream_id,
|
|
738
|
+
headers: stream[:headers],
|
|
739
|
+
body: stream[:body] || ""
|
|
740
|
+
}
|
|
741
|
+
end
|
|
759
742
|
else
|
|
760
743
|
streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, end_stream)
|
|
761
744
|
end
|
|
@@ -769,7 +752,7 @@ module Raptor
|
|
|
769
752
|
#
|
|
770
753
|
# @param streams [Hash] current open-stream map
|
|
771
754
|
# @param completed_requests [Array<Hash>] accumulator of completed stream requests
|
|
772
|
-
# @param stream_id [Integer] the stream identifier
|
|
755
|
+
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
773
756
|
# @param decoded_headers [Array<Array(String, String)>] decoded header pairs
|
|
774
757
|
# @param end_stream [Boolean] whether the source frame had END_STREAM set
|
|
775
758
|
# @return [Array(Hash, Array<Hash>)] updated streams and completed_requests
|
|
@@ -820,6 +803,7 @@ module Raptor
|
|
|
820
803
|
id: data[:id],
|
|
821
804
|
protocol: :http2,
|
|
822
805
|
buffer: buffer || "",
|
|
806
|
+
max_body_size: data[:max_body_size],
|
|
823
807
|
hpack_table: hpack_table,
|
|
824
808
|
http2_streams: streams,
|
|
825
809
|
http2_window: connection_window,
|
|
@@ -840,20 +824,95 @@ module Raptor
|
|
|
840
824
|
end
|
|
841
825
|
private_class_method :build_result
|
|
842
826
|
|
|
827
|
+
# @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
|
|
828
|
+
# @rbs @server_port_string: String
|
|
829
|
+
# @rbs @write_timeout: Integer
|
|
830
|
+
# @rbs @max_body_size: Integer?
|
|
831
|
+
# @rbs @access_log_io: IO?
|
|
832
|
+
# @rbs @clean_thread_locals: bool
|
|
833
|
+
# @rbs @clean_fiber_locals: bool
|
|
834
|
+
# @rbs @on_error: (^(Hash[String, untyped]?, Exception) -> void)?
|
|
835
|
+
# @rbs @running: AtomicBoolean
|
|
836
|
+
# @rbs @initial_settings_frame: String
|
|
837
|
+
|
|
838
|
+
# Creates a new Http2 handler.
|
|
839
|
+
#
|
|
840
|
+
# @param app [#call] the Rack application to dispatch requests to
|
|
841
|
+
# @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
|
|
842
|
+
# @param connection_options [Hash] per-connection settings shared across protocols
|
|
843
|
+
# @option connection_options [Integer] :write_timeout per-write socket timeout in seconds
|
|
844
|
+
# @option connection_options [Integer, nil] :max_body_size maximum request body size in bytes
|
|
845
|
+
# @param http2_options [Hash] HTTP/2-specific settings
|
|
846
|
+
# @option http2_options [Integer] :max_concurrent_streams maximum HTTP/2 concurrent streams per connection
|
|
847
|
+
# @param access_log_io [IO, nil] IO to write Common Log Format access entries to, or nil to disable
|
|
848
|
+
# @param clean_thread_locals [Boolean] whether to clear application thread locals after each request
|
|
849
|
+
# @param clean_fiber_locals [Boolean] whether to process each request in a fresh Fiber
|
|
850
|
+
# @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
|
|
851
|
+
# @return [void]
|
|
852
|
+
#
|
|
853
|
+
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?connection_options: Hash[Symbol, untyped], ?http2_options: Hash[Symbol, untyped], ?access_log_io: IO?, ?clean_thread_locals: bool, ?clean_fiber_locals: bool, ?on_error: (^(Hash[String, untyped]?, Exception) -> void)?) -> void
|
|
854
|
+
def initialize(app, server_port, connection_options: {}, http2_options: {}, access_log_io: nil, clean_thread_locals: true, clean_fiber_locals: false, on_error: nil)
|
|
855
|
+
@app = app
|
|
856
|
+
@server_port_string = server_port.to_s.freeze
|
|
857
|
+
@write_timeout = connection_options[:write_timeout] || Http::WRITE_TIMEOUT
|
|
858
|
+
@max_body_size = connection_options[:max_body_size]
|
|
859
|
+
@access_log_io = access_log_io
|
|
860
|
+
@clean_thread_locals = clean_thread_locals
|
|
861
|
+
@clean_fiber_locals = clean_fiber_locals
|
|
862
|
+
@on_error = on_error
|
|
863
|
+
@running = AtomicBoolean.new(true)
|
|
864
|
+
|
|
865
|
+
parser = Http2Parser.new
|
|
866
|
+
settings_payload = parser.build_settings(
|
|
867
|
+
max_concurrent_streams: http2_options[:max_concurrent_streams],
|
|
868
|
+
initial_window_size: DEFAULT_WINDOW_SIZE,
|
|
869
|
+
max_header_list_size: Http2Parser::MAX_HEADER_LIST_SIZE
|
|
870
|
+
)
|
|
871
|
+
@initial_settings_frame = parser.build_frame(:settings, 0, 0, settings_payload).freeze
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
# Returns a Ractor-safe proc that parses HTTP/2 frames from the
|
|
875
|
+
# state hash's buffered bytes.
|
|
876
|
+
#
|
|
877
|
+
# @return [Proc]
|
|
878
|
+
#
|
|
879
|
+
# @rbs () -> ^(Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
880
|
+
def parser_worker
|
|
881
|
+
proc do |data|
|
|
882
|
+
Http2.process_frames(data)
|
|
883
|
+
end
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
# Stops new stream dispatch and asks clients to finish streams already
|
|
887
|
+
# handed to the application.
|
|
888
|
+
#
|
|
889
|
+
# @param reactor [Reactor] the reactor managing HTTP/2 connections
|
|
890
|
+
# @return [void]
|
|
891
|
+
#
|
|
892
|
+
# @rbs (Reactor reactor) -> void
|
|
893
|
+
def shutdown(reactor)
|
|
894
|
+
@running.make_false
|
|
895
|
+
parser = Http2Parser.new
|
|
896
|
+
reactor.drain_http2 do |stream_id|
|
|
897
|
+
payload = [stream_id, ERROR_NO_ERROR].pack("NN")
|
|
898
|
+
parser.build_frame(:goaway, 0, 0, payload)
|
|
899
|
+
end
|
|
900
|
+
end
|
|
901
|
+
|
|
843
902
|
# Sends the server SETTINGS frame on a freshly negotiated HTTP/2
|
|
844
903
|
# connection, then eagerly reads and parses the first client frame
|
|
845
904
|
# batch, dispatching completed streams directly to the thread pool.
|
|
846
905
|
# Falls back to the reactor when no initial data is ready.
|
|
847
906
|
#
|
|
848
|
-
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
849
|
-
# @param id [Integer] unique
|
|
907
|
+
# @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
|
|
908
|
+
# @param id [Integer] unique connection identifier
|
|
850
909
|
# @param reactor [Reactor] the reactor managing the connection
|
|
851
910
|
# @param thread_pool [AtomicThreadPool] thread pool for application processing
|
|
852
|
-
# @param remote_addr [String] client IP address
|
|
853
|
-
# @param url_scheme [String] "https"
|
|
911
|
+
# @param remote_addr [String] the client IP address
|
|
912
|
+
# @param url_scheme [String] "http" or "https"
|
|
854
913
|
# @return [void]
|
|
855
914
|
#
|
|
856
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, String remote_addr, String url_scheme) -> void
|
|
915
|
+
# @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, String remote_addr, String url_scheme) -> void
|
|
857
916
|
def eager_accept(socket, id, reactor, thread_pool, remote_addr, url_scheme)
|
|
858
917
|
unless @running.true?
|
|
859
918
|
socket.close rescue nil
|
|
@@ -867,6 +926,7 @@ module Raptor
|
|
|
867
926
|
initial_state = {
|
|
868
927
|
id: id,
|
|
869
928
|
protocol: :http2,
|
|
929
|
+
max_body_size: @max_body_size,
|
|
870
930
|
remote_addr: remote_addr,
|
|
871
931
|
url_scheme: url_scheme
|
|
872
932
|
}
|
|
@@ -901,7 +961,7 @@ module Raptor
|
|
|
901
961
|
buffer << socket.read_nonblock(socket.pending)
|
|
902
962
|
end
|
|
903
963
|
|
|
904
|
-
result =
|
|
964
|
+
result = Http2.process_frames(initial_state.merge(buffer: buffer))
|
|
905
965
|
handle_parsed_request(result, reactor, thread_pool)
|
|
906
966
|
rescue => error
|
|
907
967
|
Log.rescued_error(error)
|
|
@@ -913,45 +973,45 @@ module Raptor
|
|
|
913
973
|
# further buffered frame batches before returning control to the
|
|
914
974
|
# reactor.
|
|
915
975
|
#
|
|
916
|
-
# @param
|
|
976
|
+
# @param parsed_request [Hash] the parsed result produced by {.process_frames}
|
|
917
977
|
# @param reactor [Reactor] the reactor managing the connection
|
|
918
|
-
# @param thread_pool [AtomicThreadPool] thread pool for
|
|
978
|
+
# @param thread_pool [AtomicThreadPool] thread pool for application processing
|
|
919
979
|
# @return [void]
|
|
920
980
|
#
|
|
921
|
-
# @rbs (Hash[Symbol, untyped]
|
|
922
|
-
def handle_parsed_request(
|
|
923
|
-
socket = reactor.socket_for(
|
|
981
|
+
# @rbs (Hash[Symbol, untyped] parsed_request, Reactor reactor, AtomicThreadPool thread_pool) -> void
|
|
982
|
+
def handle_parsed_request(parsed_request, reactor, thread_pool)
|
|
983
|
+
socket = reactor.socket_for(parsed_request[:id])
|
|
924
984
|
return unless socket
|
|
925
985
|
|
|
926
|
-
writer = reactor.writer_for(
|
|
927
|
-
flow_control = reactor.flow_control_for(
|
|
986
|
+
writer = reactor.writer_for(parsed_request[:id])
|
|
987
|
+
flow_control = reactor.flow_control_for(parsed_request[:id])
|
|
928
988
|
|
|
929
989
|
rounds = 0
|
|
930
990
|
loop do
|
|
931
|
-
reactor.acknowledge_http2_ping(
|
|
932
|
-
|
|
991
|
+
reactor.acknowledge_http2_ping(parsed_request[:id], parsed_request[:ping_acknowledgements])
|
|
992
|
+
parsed_request[:cancelled_streams]&.each do |stream_id|
|
|
933
993
|
flow_control.cancel_stream(stream_id)
|
|
934
|
-
reactor.cancel_http2_body(
|
|
994
|
+
reactor.cancel_http2_body(parsed_request[:id], stream_id)
|
|
935
995
|
end
|
|
936
996
|
|
|
937
|
-
if flow_control && (
|
|
938
|
-
apply_flow_control_updates(flow_control,
|
|
939
|
-
reactor.resume_http2_bodies(
|
|
997
|
+
if flow_control && (parsed_request[:window_updates]&.any? || parsed_request[:peer_initial_window_size])
|
|
998
|
+
apply_flow_control_updates(flow_control, parsed_request)
|
|
999
|
+
reactor.resume_http2_bodies(parsed_request[:id])
|
|
940
1000
|
end
|
|
941
1001
|
|
|
942
|
-
writer.write_frames(socket,
|
|
1002
|
+
writer.write_frames(socket, parsed_request[:outgoing_frames])
|
|
943
1003
|
|
|
944
|
-
if
|
|
945
|
-
reactor.close_connection(
|
|
1004
|
+
if parsed_request[:close_connection]
|
|
1005
|
+
reactor.close_connection(parsed_request[:id])
|
|
946
1006
|
return
|
|
947
1007
|
end
|
|
948
1008
|
|
|
949
|
-
|
|
1009
|
+
parsed_request[:completed_requests]&.each do |request|
|
|
950
1010
|
stream_id = request[:stream_id]
|
|
951
|
-
remote_addr =
|
|
1011
|
+
remote_addr = parsed_request[:remote_addr] || Server::DEFAULT_REMOTE_ADDR
|
|
952
1012
|
|
|
953
|
-
unless reactor.dispatch_http2_stream(
|
|
954
|
-
|
|
1013
|
+
unless reactor.dispatch_http2_stream(parsed_request[:id], stream_id)
|
|
1014
|
+
write_reset_stream(socket, writer, stream_id, ERROR_REFUSED_STREAM)
|
|
955
1015
|
next
|
|
956
1016
|
end
|
|
957
1017
|
|
|
@@ -971,19 +1031,28 @@ module Raptor
|
|
|
971
1031
|
next_batch = eager_read_next_batch(socket)
|
|
972
1032
|
break unless next_batch
|
|
973
1033
|
|
|
974
|
-
|
|
1034
|
+
parsed_request = Http2.process_frames(parsed_request.merge(buffer: parsed_request[:buffer] + next_batch))
|
|
975
1035
|
end
|
|
976
1036
|
|
|
977
|
-
reactor.
|
|
1037
|
+
reactor.update_state(parsed_request)
|
|
978
1038
|
end
|
|
979
1039
|
|
|
980
1040
|
private
|
|
981
1041
|
|
|
1042
|
+
# Creates a per-connection {Writer} configured with the handler's write timeout.
|
|
1043
|
+
#
|
|
1044
|
+
# @return [Writer] a new per-connection frame writer
|
|
1045
|
+
#
|
|
1046
|
+
# @rbs () -> Writer
|
|
1047
|
+
def create_writer
|
|
1048
|
+
Writer.new(write_timeout: @write_timeout)
|
|
1049
|
+
end
|
|
1050
|
+
|
|
982
1051
|
# Applies inbound flow-control updates from a parsed result to the
|
|
983
|
-
# connection's
|
|
1052
|
+
# connection's {FlowControl}.
|
|
984
1053
|
#
|
|
985
|
-
# @param flow_control [FlowControl]
|
|
986
|
-
# @param result [Hash] the parsed result from
|
|
1054
|
+
# @param flow_control [FlowControl] per-connection outbound flow controller
|
|
1055
|
+
# @param result [Hash] the parsed result from {.process_frames}
|
|
987
1056
|
# @return [void]
|
|
988
1057
|
#
|
|
989
1058
|
# @rbs (FlowControl flow_control, Hash[Symbol, untyped] result) -> void
|
|
@@ -1004,22 +1073,19 @@ module Raptor
|
|
|
1004
1073
|
# Reads the next frame batch from `socket` if it has already arrived,
|
|
1005
1074
|
# without waiting on the shared collector thread.
|
|
1006
1075
|
#
|
|
1007
|
-
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
1076
|
+
# @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
|
|
1008
1077
|
# @return [String, nil] the bytes read, or nil if nothing was available
|
|
1009
1078
|
#
|
|
1010
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket) -> String?
|
|
1079
|
+
# @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket) -> String?
|
|
1011
1080
|
def eager_read_next_batch(socket)
|
|
1012
1081
|
return unless socket.wait_readable(0)
|
|
1013
1082
|
|
|
1014
|
-
|
|
1083
|
+
buffer = begin
|
|
1015
1084
|
socket.read_nonblock(EAGER_READ_BUFFER_SIZE)
|
|
1016
1085
|
rescue IO::WaitReadable, EOFError, IOError, SystemCallError
|
|
1017
1086
|
return
|
|
1018
1087
|
end
|
|
1019
1088
|
|
|
1020
|
-
buffer = String.new
|
|
1021
|
-
buffer << data
|
|
1022
|
-
|
|
1023
1089
|
while socket.respond_to?(:pending) && socket.pending.positive?
|
|
1024
1090
|
buffer << socket.read_nonblock(socket.pending)
|
|
1025
1091
|
end
|
|
@@ -1030,8 +1096,8 @@ module Raptor
|
|
|
1030
1096
|
# Dispatches a completed stream request to the Rack app and writes
|
|
1031
1097
|
# the response back as HTTP/2 frames.
|
|
1032
1098
|
#
|
|
1033
|
-
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
1034
|
-
# @param writer [Writer]
|
|
1099
|
+
# @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
|
|
1100
|
+
# @param writer [Writer] frame writer for the connection
|
|
1035
1101
|
# @param flow_control [FlowControl] per-connection outbound flow controller
|
|
1036
1102
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
1037
1103
|
# @param headers [Array<Array(String, String)>] request headers
|
|
@@ -1039,12 +1105,12 @@ module Raptor
|
|
|
1039
1105
|
# @param remote_addr [String] the client IP address
|
|
1040
1106
|
# @return [void]
|
|
1041
1107
|
#
|
|
1042
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
|
|
1108
|
+
# @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
|
|
1043
1109
|
def dispatch_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:)
|
|
1044
1110
|
if @clean_fiber_locals
|
|
1045
|
-
Fiber.new { perform_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:) }.resume
|
|
1111
|
+
Fiber.new { perform_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr: remote_addr) }.resume
|
|
1046
1112
|
else
|
|
1047
|
-
perform_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:)
|
|
1113
|
+
perform_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr: remote_addr)
|
|
1048
1114
|
end
|
|
1049
1115
|
ensure
|
|
1050
1116
|
ThreadLocals.clear if @clean_thread_locals
|
|
@@ -1052,8 +1118,8 @@ module Raptor
|
|
|
1052
1118
|
|
|
1053
1119
|
# Calls the Rack app and writes its response for one stream.
|
|
1054
1120
|
#
|
|
1055
|
-
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
1056
|
-
# @param writer [Writer]
|
|
1121
|
+
# @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
|
|
1122
|
+
# @param writer [Writer] frame writer for the connection
|
|
1057
1123
|
# @param flow_control [FlowControl] per-connection outbound flow controller
|
|
1058
1124
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
1059
1125
|
# @param headers [Array<Array(String, String)>] request headers
|
|
@@ -1061,7 +1127,7 @@ module Raptor
|
|
|
1061
1127
|
# @param remote_addr [String] the client IP address
|
|
1062
1128
|
# @return [void]
|
|
1063
1129
|
#
|
|
1064
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
|
|
1130
|
+
# @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
|
|
1065
1131
|
def perform_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:)
|
|
1066
1132
|
env = nil
|
|
1067
1133
|
status = nil
|
|
@@ -1081,14 +1147,14 @@ module Raptor
|
|
|
1081
1147
|
Http.call_response_finished(env, status, response_headers, error)
|
|
1082
1148
|
flow_control.discard_stream(stream_id)
|
|
1083
1149
|
end
|
|
1084
|
-
|
|
1150
|
+
write_detached_response(socket, writer, flow_control, stream_id, status, response_headers, response_body, finished) do
|
|
1085
1151
|
response_started = true
|
|
1086
1152
|
end
|
|
1087
1153
|
detached = true
|
|
1088
1154
|
return
|
|
1089
1155
|
end
|
|
1090
1156
|
|
|
1091
|
-
response_size =
|
|
1157
|
+
response_size = write_response(
|
|
1092
1158
|
socket,
|
|
1093
1159
|
writer,
|
|
1094
1160
|
flow_control,
|
|
@@ -1099,11 +1165,6 @@ module Raptor
|
|
|
1099
1165
|
trailers: env[RESPONSE_TRAILERS],
|
|
1100
1166
|
request_method: env[Rack::REQUEST_METHOD]
|
|
1101
1167
|
) { response_started = true }
|
|
1102
|
-
if response_body.is_a?(DetachedBody)
|
|
1103
|
-
response_body.finish(:closed)
|
|
1104
|
-
elsif response_body.respond_to?(:close)
|
|
1105
|
-
response_body.close
|
|
1106
|
-
end
|
|
1107
1168
|
write_access_log(env, status, response_size, remote_addr) if @access_log_io
|
|
1108
1169
|
Http.call_response_finished(env, status, response_headers, nil)
|
|
1109
1170
|
rescue StreamClosedError => error
|
|
@@ -1111,9 +1172,9 @@ module Raptor
|
|
|
1111
1172
|
rescue => error
|
|
1112
1173
|
Http.call_response_finished(env, status, response_headers, error)
|
|
1113
1174
|
if response_started
|
|
1114
|
-
|
|
1175
|
+
write_reset_stream(socket, writer, stream_id, ERROR_INTERNAL_ERROR)
|
|
1115
1176
|
else
|
|
1116
|
-
|
|
1177
|
+
write_error_response(socket, writer, stream_id)
|
|
1117
1178
|
end
|
|
1118
1179
|
|
|
1119
1180
|
if @on_error
|
|
@@ -1127,8 +1188,8 @@ module Raptor
|
|
|
1127
1188
|
|
|
1128
1189
|
# Starts a detached HTTP/2 response after writing its response headers.
|
|
1129
1190
|
#
|
|
1130
|
-
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
1131
|
-
# @param writer [Writer]
|
|
1191
|
+
# @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
|
|
1192
|
+
# @param writer [Writer] frame writer for the connection
|
|
1132
1193
|
# @param flow_control [FlowControl] per-connection outbound flow controller
|
|
1133
1194
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
1134
1195
|
# @param status [Integer] HTTP status code
|
|
@@ -1138,22 +1199,22 @@ module Raptor
|
|
|
1138
1199
|
# @yield once the response headers are queued
|
|
1139
1200
|
# @return [void]
|
|
1140
1201
|
#
|
|
1141
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, DetachedBody body, ^(Symbol) -> void finished) { () -> void } -> void
|
|
1142
|
-
def
|
|
1202
|
+
# @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, DetachedBody body, ^(Symbol) -> void finished) { () -> void } -> void
|
|
1203
|
+
def write_detached_response(socket, writer, flow_control, stream_id, status, headers, body, finished)
|
|
1143
1204
|
flow_control.check(stream_id)
|
|
1144
1205
|
parser = Http2Parser.new
|
|
1145
1206
|
encoded = parser.encode_response_headers(status, headers)
|
|
1146
1207
|
writer.write_frames(socket, [parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded)])
|
|
1147
1208
|
yield
|
|
1148
1209
|
attached = writer.attach_body(stream_id, body, finished)
|
|
1149
|
-
|
|
1210
|
+
write_reset_stream(socket, writer, stream_id, ERROR_REFUSED_STREAM) unless attached
|
|
1150
1211
|
end
|
|
1151
1212
|
|
|
1152
1213
|
# Writes a Rack response as HTTP/2 frames to the socket, partitioning
|
|
1153
1214
|
# DATA frames through `flow_control` to fit within the peer's windows.
|
|
1154
1215
|
#
|
|
1155
|
-
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
1156
|
-
# @param writer [Writer]
|
|
1216
|
+
# @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
|
|
1217
|
+
# @param writer [Writer] frame writer for the connection
|
|
1157
1218
|
# @param flow_control [FlowControl] per-connection outbound flow controller
|
|
1158
1219
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
1159
1220
|
# @param status [Integer] HTTP status code
|
|
@@ -1161,10 +1222,11 @@ module Raptor
|
|
|
1161
1222
|
# @param body [Object] response body responding to each
|
|
1162
1223
|
# @param trailers [Hash] trailing response headers populated by the Rack application
|
|
1163
1224
|
# @param request_method [String] request method used to suppress HEAD response bodies
|
|
1225
|
+
# @yield once the response headers are queued
|
|
1164
1226
|
# @return [String] the response body size in bytes
|
|
1165
1227
|
#
|
|
1166
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body, trailers: Hash[String, String | Array[String]], request_method: String) ?{ () -> void } -> String
|
|
1167
|
-
def
|
|
1228
|
+
# @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body, trailers: Hash[String, String | Array[String]], request_method: String) ?{ () -> void } -> String
|
|
1229
|
+
def write_response(socket, writer, flow_control, stream_id, status, headers, body, trailers:, request_method:)
|
|
1168
1230
|
parser = Http2Parser.new
|
|
1169
1231
|
|
|
1170
1232
|
encoded_headers = parser.encode_response_headers(status, headers)
|
|
@@ -1188,18 +1250,24 @@ module Raptor
|
|
|
1188
1250
|
stream.close
|
|
1189
1251
|
|
|
1190
1252
|
stream.bytes_written.to_s
|
|
1253
|
+
ensure
|
|
1254
|
+
if body.is_a?(DetachedBody)
|
|
1255
|
+
body.finish($! ? :cancelled : :closed)
|
|
1256
|
+
elsif body.respond_to?(:close)
|
|
1257
|
+
body.close
|
|
1258
|
+
end
|
|
1191
1259
|
end
|
|
1192
1260
|
|
|
1193
1261
|
# Sends an HTTP 103 Early Hints response.
|
|
1194
1262
|
#
|
|
1195
|
-
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
1263
|
+
# @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
|
|
1196
1264
|
# @param writer [Writer] frame writer for the connection
|
|
1197
1265
|
# @param flow_control [FlowControl] per-connection outbound flow controller
|
|
1198
1266
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
1199
1267
|
# @param hints [Hash] response headers to send as early hints
|
|
1200
1268
|
# @return [void]
|
|
1201
1269
|
#
|
|
1202
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Hash[String, String | Array[String]] hints) -> void
|
|
1270
|
+
# @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Hash[String, String | Array[String]] hints) -> void
|
|
1203
1271
|
def send_early_hints(socket, writer, flow_control, stream_id, hints)
|
|
1204
1272
|
return if hints.empty?
|
|
1205
1273
|
|
|
@@ -1211,14 +1279,14 @@ module Raptor
|
|
|
1211
1279
|
|
|
1212
1280
|
# Resets one HTTP/2 stream with the given error code.
|
|
1213
1281
|
#
|
|
1214
|
-
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
1282
|
+
# @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
|
|
1215
1283
|
# @param writer [Writer] frame writer for the connection
|
|
1216
1284
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
1217
1285
|
# @param error_code [Integer] the HTTP/2 error code
|
|
1218
1286
|
# @return [void]
|
|
1219
1287
|
#
|
|
1220
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Integer error_code) -> void
|
|
1221
|
-
def
|
|
1288
|
+
# @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Integer error_code) -> void
|
|
1289
|
+
def write_reset_stream(socket, writer, stream_id, error_code)
|
|
1222
1290
|
parser = Http2Parser.new
|
|
1223
1291
|
frame = parser.build_frame(:rst_stream, 0, stream_id, [error_code].pack("N"))
|
|
1224
1292
|
writer.write_frames(socket, [frame])
|
|
@@ -1226,13 +1294,13 @@ module Raptor
|
|
|
1226
1294
|
|
|
1227
1295
|
# Writes a 500 error response as HTTP/2 frames.
|
|
1228
1296
|
#
|
|
1229
|
-
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
1230
|
-
# @param writer [Writer]
|
|
1297
|
+
# @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
|
|
1298
|
+
# @param writer [Writer] frame writer for the connection
|
|
1231
1299
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
1232
1300
|
# @return [void]
|
|
1233
1301
|
#
|
|
1234
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id) -> void
|
|
1235
|
-
def
|
|
1302
|
+
# @rbs (TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id) -> void
|
|
1303
|
+
def write_error_response(socket, writer, stream_id)
|
|
1236
1304
|
parser = Http2Parser.new
|
|
1237
1305
|
encoded = parser.encode_headers([[":status", "500"]])
|
|
1238
1306
|
|
|
@@ -1242,33 +1310,21 @@ module Raptor
|
|
|
1242
1310
|
)
|
|
1243
1311
|
end
|
|
1244
1312
|
|
|
1245
|
-
# Instance-level wrapper around {Http.write_access_log} that routes to
|
|
1246
|
-
# the configured `@access_log_io`.
|
|
1247
|
-
#
|
|
1248
|
-
# @param env [Hash] the Rack environment
|
|
1249
|
-
# @param status [Integer] the response status code
|
|
1250
|
-
# @param size [String] the response body size in bytes, or `-` if unknown
|
|
1251
|
-
# @param remote_addr [String] the client IP address
|
|
1252
|
-
# @return [void]
|
|
1253
|
-
#
|
|
1254
|
-
# @rbs (Hash[String, untyped] env, Integer status, String size, String remote_addr) -> void
|
|
1255
|
-
def write_access_log(env, status, size, remote_addr)
|
|
1256
|
-
Http.write_access_log(@access_log_io, env, status, size, remote_addr)
|
|
1257
|
-
end
|
|
1258
|
-
|
|
1259
1313
|
# Builds a Rack environment hash from HTTP/2 headers and body.
|
|
1260
1314
|
#
|
|
1261
1315
|
# @param headers [Array<Array(String, String)>] HTTP/2 header pairs
|
|
1262
1316
|
# @param body [String] the request body
|
|
1263
|
-
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
1317
|
+
# @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the connection socket
|
|
1264
1318
|
# @param writer [Writer] frame writer for the connection
|
|
1319
|
+
# @param flow_control [FlowControl] per-connection outbound flow controller
|
|
1265
1320
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
1266
1321
|
# @param remote_addr [String] the client IP address
|
|
1267
1322
|
# @return [Hash] fully populated Rack environment hash
|
|
1268
1323
|
#
|
|
1269
|
-
# @rbs (Array[[String, String]] headers, String body, OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, remote_addr: String) -> Hash[String, untyped]
|
|
1324
|
+
# @rbs (Array[[String, String]] headers, String body, TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, remote_addr: String) -> Hash[String, untyped]
|
|
1270
1325
|
def build_rack_env(headers, body, socket, writer, flow_control, stream_id, remote_addr:)
|
|
1271
1326
|
env = {}
|
|
1327
|
+
underscored_headers = nil
|
|
1272
1328
|
|
|
1273
1329
|
headers.each do |name, value|
|
|
1274
1330
|
if name.start_with?(":")
|
|
@@ -1287,7 +1343,19 @@ module Raptor
|
|
|
1287
1343
|
env[Http::CONTENT_LENGTH] = value
|
|
1288
1344
|
else
|
|
1289
1345
|
rack_key = "HTTP_#{name.upcase.tr("-", "_")}"
|
|
1290
|
-
|
|
1346
|
+
if name.include?("_")
|
|
1347
|
+
next if env.key?(rack_key) && !underscored_headers&.key?(rack_key)
|
|
1348
|
+
|
|
1349
|
+
(underscored_headers ||= {})[rack_key] = true
|
|
1350
|
+
elsif underscored_headers&.delete(rack_key)
|
|
1351
|
+
env.delete(rack_key)
|
|
1352
|
+
end
|
|
1353
|
+
if (existing = env[rack_key])
|
|
1354
|
+
separator = rack_key == Rack::HTTP_COOKIE ? "; " : ", "
|
|
1355
|
+
env[rack_key] = "#{existing}#{separator}#{value}"
|
|
1356
|
+
else
|
|
1357
|
+
env[rack_key] = value
|
|
1358
|
+
end
|
|
1291
1359
|
end
|
|
1292
1360
|
end
|
|
1293
1361
|
|
|
@@ -1311,7 +1379,6 @@ module Raptor
|
|
|
1311
1379
|
|
|
1312
1380
|
env[Http::REMOTE_ADDR] = remote_addr
|
|
1313
1381
|
env[Http::SERVER_SOFTWARE] = Http::SERVER_SOFTWARE_VALUE
|
|
1314
|
-
env[Http::HTTP_VERSION] = SERVER_PROTOCOL
|
|
1315
1382
|
env[RESPONSE_TRAILERS] = {}
|
|
1316
1383
|
|
|
1317
1384
|
populate_server_name_and_port(env)
|
|
@@ -1335,12 +1402,25 @@ module Raptor
|
|
|
1335
1402
|
else
|
|
1336
1403
|
host, port = http_host.split(":", 2)
|
|
1337
1404
|
end
|
|
1338
|
-
env[Rack::SERVER_NAME] ||= host
|
|
1339
|
-
env[Rack::SERVER_PORT] ||= port || @
|
|
1405
|
+
env[Rack::SERVER_NAME] ||= host || Server::DEFAULT_SERVER_NAME
|
|
1406
|
+
env[Rack::SERVER_PORT] ||= port || @server_port_string
|
|
1340
1407
|
else
|
|
1341
1408
|
env[Rack::SERVER_NAME] ||= Server::DEFAULT_SERVER_NAME
|
|
1342
|
-
env[Rack::SERVER_PORT] ||= @
|
|
1409
|
+
env[Rack::SERVER_PORT] ||= @server_port_string
|
|
1343
1410
|
end
|
|
1344
1411
|
end
|
|
1412
|
+
|
|
1413
|
+
# Writes a completed request to the access log.
|
|
1414
|
+
#
|
|
1415
|
+
# @param env [Hash] the Rack environment
|
|
1416
|
+
# @param status [Integer] the response status code
|
|
1417
|
+
# @param size [String] the response body size in bytes, or `-` if unknown
|
|
1418
|
+
# @param remote_addr [String] the client IP address
|
|
1419
|
+
# @return [void]
|
|
1420
|
+
#
|
|
1421
|
+
# @rbs (Hash[String, untyped] env, Integer status, String size, String remote_addr) -> void
|
|
1422
|
+
def write_access_log(env, status, size, remote_addr)
|
|
1423
|
+
Http.write_access_log(@access_log_io, env, status, size, remote_addr)
|
|
1424
|
+
end
|
|
1345
1425
|
end
|
|
1346
1426
|
end
|