raptor 0.23.0 → 0.25.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.
@@ -9,7 +9,7 @@ require "atomic-ruby/atomic_boolean"
9
9
  require "atomic-ruby/atomic_condition_variable"
10
10
  require "atomic-ruby/atomic_queue"
11
11
  require "atomic-ruby/atomic_thread_pool"
12
- require "nio"
12
+ require "io/event"
13
13
  require "red-black-tree"
14
14
 
15
15
  require_relative "detached_body"
@@ -76,7 +76,6 @@ module Raptor
76
76
  attr_accessor :wait #: Symbol?
77
77
  attr_accessor :reading #: bool
78
78
  attr_accessor :closing #: bool
79
- attr_accessor :monitor #: NIO::Monitor?
80
79
  attr_accessor :timeout #: TimeoutClient?
81
80
  attr_reader :output #: Array[String]
82
81
 
@@ -91,7 +90,6 @@ module Raptor
91
90
  @wait = nil
92
91
  @reading = false
93
92
  @closing = false
94
- @monitor = nil
95
93
  @timeout = nil
96
94
  end
97
95
 
@@ -182,7 +180,10 @@ module Raptor
182
180
  # @rbs @persistent_data_timeout: Integer
183
181
  # @rbs @http2_keepalive_interval: Integer
184
182
  # @rbs @http2_keepalive_timeout: Integer
185
- # @rbs @selector: NIO::Selector
183
+ # @rbs @selector: untyped
184
+ # @rbs @wakeup: Fiber?
185
+ # @rbs @waiters: Hash[TCPSocket, [Fiber, Integer]]
186
+ # @rbs @ready: Array[[TCPSocket, Integer]]
186
187
  # @rbs @queue: Queue[TCPSocket]
187
188
  # @rbs @io_queue: AtomicQueue
188
189
  # @rbs @io_applied: AtomicConditionVariable
@@ -231,7 +232,10 @@ module Raptor
231
232
  @http2_keepalive_interval = http2_options[:keepalive_interval]
232
233
  @http2_keepalive_timeout = http2_options[:keepalive_timeout]
233
234
 
234
- @selector = NIO::Selector.new
235
+ @selector = nil
236
+ @wakeup = nil
237
+ @waiters = {}
238
+ @ready = []
235
239
  @queue = Queue.new
236
240
  @io_queue = AtomicQueue.new
237
241
  @io_applied = AtomicConditionVariable.new
@@ -262,13 +266,19 @@ module Raptor
262
266
  Thread.new do
263
267
  Thread.current.name = "Reactor"
264
268
  @thread = Thread.current
269
+ selector = IO::Event::Selector.new(Fiber.current)
270
+ @wakeup = Fiber.new { loop { selector.transfer } }
271
+ @selector = selector
265
272
 
266
273
  until stopped?
267
274
  begin
268
275
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
269
276
  timeout = @timeouts.min&.timeout(now)
270
- @selector.select(timeout) do |monitor|
271
- handle_monitor(monitor)
277
+ timeout = 0 unless @ready.empty? && @queue.empty? && @io_queue.empty?
278
+ @selector.select(timeout)
279
+ until @ready.empty?
280
+ socket, events = @ready.shift
281
+ handle_ready(socket, events)
272
282
  end
273
283
 
274
284
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -283,6 +293,8 @@ module Raptor
283
293
  end
284
294
  end
285
295
 
296
+ @waiters.keys.each { |socket| deregister(socket) }
297
+ @ready.clear
286
298
  @id_to_flow_control.each_value(&:close)
287
299
  @id_to_socket.each_value { |socket| socket.close rescue nil }
288
300
  @id_to_socket.clear
@@ -330,7 +342,7 @@ module Raptor
330
342
 
331
343
  @socket_to_state[socket] = state
332
344
  @queue << socket
333
- @selector.wakeup
345
+ wakeup
334
346
  rescue ClosedQueueError
335
347
  socket.close
336
348
  end
@@ -372,7 +384,7 @@ module Raptor
372
384
  @id_to_socket[id] = socket
373
385
  @socket_to_state[socket] = state
374
386
  @queue << socket
375
- @selector.wakeup
387
+ wakeup
376
388
  rescue ClosedQueueError
377
389
  socket.close
378
390
  end
@@ -422,7 +434,7 @@ module Raptor
422
434
  def attach_http1_body(socket, id, body, state, finished)
423
435
  attached = AtomicBoolean.new(false)
424
436
  @io_queue << [:attach_http1, id, socket, body, attached, finished, state]
425
- @selector.wakeup rescue nil
437
+ wakeup rescue nil
426
438
  @io_applied.wait { attached.true? }
427
439
  return false if body.closed?
428
440
 
@@ -430,7 +442,7 @@ module Raptor
430
442
  true
431
443
  rescue
432
444
  @io_queue << [:cancel_http1, id]
433
- @selector.wakeup rescue nil
445
+ wakeup rescue nil
434
446
  raise
435
447
  end
436
448
 
@@ -490,7 +502,7 @@ module Raptor
490
502
  flush_output(id, io) if io && !io.wait
491
503
  else
492
504
  @io_queue << [:write, id, data]
493
- @selector.wakeup rescue nil
505
+ wakeup rescue nil
494
506
  end
495
507
  end
496
508
 
@@ -506,7 +518,7 @@ module Raptor
506
518
  def attach_http2_body(id, stream_id, body, finished)
507
519
  attached = AtomicBoolean.new(false)
508
520
  @io_queue << [:attach_http2, id, stream_id, body, attached, finished]
509
- @selector.wakeup rescue nil
521
+ wakeup rescue nil
510
522
  @io_applied.wait { attached.true? }
511
523
  return false if body.closed?
512
524
 
@@ -527,7 +539,7 @@ module Raptor
527
539
  return if @detached_count.value.zero?
528
540
 
529
541
  @io_queue << [:resume_http2, id]
530
- @selector.wakeup rescue nil
542
+ wakeup rescue nil
531
543
  end
532
544
 
533
545
  # Cancels a detached response stream.
@@ -541,7 +553,7 @@ module Raptor
541
553
  return if @detached_count.value.zero?
542
554
 
543
555
  @io_queue << [:cancel_http2, id, stream_id]
544
- @selector.wakeup rescue nil
556
+ wakeup rescue nil
545
557
  end
546
558
 
547
559
  # Waits for detached responses to finish, then cancels any that outlive
@@ -554,7 +566,7 @@ module Raptor
554
566
  def drain_detached_bodies(timeout)
555
567
  barrier = AtomicBoolean.new(false)
556
568
  @io_queue << [:barrier, nil, barrier]
557
- @selector.wakeup rescue nil
569
+ wakeup rescue nil
558
570
  @io_applied.wait { barrier.true? }
559
571
 
560
572
  Timeout.timeout(timeout) do
@@ -562,7 +574,7 @@ module Raptor
562
574
  end
563
575
  rescue Timeout::Error
564
576
  @io_queue << [:cancel_all]
565
- @selector.wakeup rescue nil
577
+ wakeup rescue nil
566
578
  @detached_drained.wait { @detached_count.value.zero? }
567
579
  end
568
580
 
@@ -617,7 +629,7 @@ module Raptor
617
629
  return unless socket
618
630
 
619
631
  @queue << socket
620
- @selector.wakeup
632
+ wakeup
621
633
  rescue ClosedQueueError
622
634
  socket.close
623
635
  end
@@ -630,7 +642,7 @@ module Raptor
630
642
  # @rbs (Integer id) -> void
631
643
  def close_connection(id)
632
644
  @io_queue << [:close, id]
633
- @selector.wakeup rescue nil
645
+ wakeup rescue nil
634
646
  end
635
647
 
636
648
  # Closes the registration queue and wakes the selector so the
@@ -641,7 +653,7 @@ module Raptor
641
653
  # @rbs () -> void
642
654
  def shutdown
643
655
  @queue.close
644
- @selector.wakeup
656
+ wakeup
645
657
  end
646
658
 
647
659
  # Returns the number of complete requests either being processed
@@ -656,6 +668,17 @@ module Raptor
656
668
 
657
669
  private
658
670
 
671
+ # Wakes the event loop, including work queued before it starts waiting.
672
+ #
673
+ # @rbs () -> void
674
+ def wakeup
675
+ selector = @selector
676
+ return unless selector
677
+
678
+ selector.push(@wakeup)
679
+ selector.wakeup
680
+ end
681
+
659
682
  # Returns whether shutdown has no registrations or writes left to drain.
660
683
  #
661
684
  # @return [Boolean]
@@ -760,7 +783,7 @@ module Raptor
760
783
  dispatch = proc { |callback| @thread_pool << callback }
761
784
  wake = proc do
762
785
  @io_queue << [:ready_http1, id]
763
- @selector.wakeup rescue nil
786
+ wakeup rescue nil
764
787
  end
765
788
  unless body.attach([io.budget, @detached_budget], wake, dispatch, finished)
766
789
  socket.close rescue nil
@@ -771,7 +794,7 @@ module Raptor
771
794
  @socket_to_state[socket] = { id: id, protocol: :http1, detached: true }
772
795
  @id_to_io[id] = io
773
796
  @detached_count.swap { |count| count + 1 }
774
- update_monitor(id, io)
797
+ update_interests(id, io)
775
798
  end
776
799
 
777
800
  # Writes buffered HTTP/1.x body chunks without blocking.
@@ -803,7 +826,7 @@ module Raptor
803
826
 
804
827
  if !io.finishing && !io.wait && io.body&.next_size
805
828
  @io_queue << [:ready_http1, id]
806
- @selector.wakeup rescue nil
829
+ wakeup rescue nil
807
830
  end
808
831
  end
809
832
 
@@ -823,7 +846,7 @@ module Raptor
823
846
  body.finish(:closed)
824
847
  detached_body_removed
825
848
  @id_to_io.delete(id)
826
- deregister(socket, io)
849
+ deregister(socket)
827
850
  clear_write_timeout(io)
828
851
 
829
852
  if io.state[:keep_alive]
@@ -887,7 +910,7 @@ module Raptor
887
910
 
888
911
  wake = proc do
889
912
  @io_queue << [:ready_http2, id, stream_id]
890
- @selector.wakeup rescue nil
913
+ wakeup rescue nil
891
914
  end
892
915
  return unless body.attach([io.budget, @detached_budget], wake, dispatch, finished)
893
916
 
@@ -967,7 +990,7 @@ module Raptor
967
990
 
968
991
  if !io.wait && io.output.empty? && !io.ready.empty?
969
992
  @io_queue << [:flush_http2, id]
970
- @selector.wakeup rescue nil
993
+ wakeup rescue nil
971
994
  end
972
995
  end
973
996
 
@@ -1133,7 +1156,7 @@ module Raptor
1133
1156
  elsif io.closing
1134
1157
  remove_connection(id)
1135
1158
  else
1136
- update_monitor(id, io)
1159
+ update_interests(id, io)
1137
1160
  end
1138
1161
  end
1139
1162
 
@@ -1148,7 +1171,7 @@ module Raptor
1148
1171
  def wait_for_write(id, io, readiness)
1149
1172
  io.wait = readiness
1150
1173
  track_write_timeout(id, io)
1151
- update_monitor(id, io)
1174
+ update_interests(id, io)
1152
1175
  end
1153
1176
 
1154
1177
  # Updates selector interest for one reactor-owned connection.
@@ -1158,26 +1181,34 @@ module Raptor
1158
1181
  # @return [void]
1159
1182
  #
1160
1183
  # @rbs (Integer id, ConnectionIO io) -> void
1161
- def update_monitor(id, io)
1184
+ def update_interests(id, io)
1162
1185
  socket = @id_to_socket[id]
1163
1186
  return unless socket && io
1164
1187
 
1165
- read = io.reading || io.wait == :read
1166
- write = io.wait == :write
1167
- interests = if read && write
1168
- :rw
1169
- elsif read
1170
- :r
1171
- elsif write
1172
- :w
1173
- end
1188
+ events = 0
1189
+ events |= IO::READABLE if io.reading || io.wait == :read
1190
+ events |= IO::WRITABLE if io.wait == :write
1191
+ wait_for_io(socket, events)
1192
+ end
1174
1193
 
1175
- if io.monitor
1176
- io.monitor.interests = interests
1177
- elsif interests
1178
- io.monitor = @selector.register(socket, interests)
1179
- io.monitor.value = socket
1194
+ # Waits for a connection's next readable or writable event.
1195
+ #
1196
+ # @rbs (TCPSocket socket, Integer events) -> void
1197
+ def wait_for_io(socket, events)
1198
+ waiter = @waiters[socket]
1199
+ return if waiter && waiter[1] == events
1200
+
1201
+ deregister(socket)
1202
+ return if events.zero?
1203
+
1204
+ fiber = Fiber.new do
1205
+ readiness = @selector.io_wait(Fiber.current, socket, events)
1206
+ @ready << [socket, readiness] if readiness
1207
+ ensure
1208
+ @waiters.delete(socket)
1180
1209
  end
1210
+ @waiters[socket] = [fiber, events]
1211
+ fiber.transfer
1181
1212
  end
1182
1213
 
1183
1214
  # Starts or refreshes one connection's non-blocking write deadline.
@@ -1210,19 +1241,15 @@ module Raptor
1210
1241
  # Removes a socket from selector tracking.
1211
1242
  #
1212
1243
  # @param socket [TCPSocket] socket to deregister
1213
- # @param io [ConnectionIO, nil] connection I/O state
1214
1244
  # @return [void]
1215
1245
  #
1216
- # @rbs (TCPSocket socket, ConnectionIO? io) -> void
1217
- def deregister(socket, io)
1218
- return unless io&.monitor || @selector.registered?(socket)
1219
-
1220
- @selector.deregister(socket)
1221
- io.monitor = nil if io
1222
- rescue IOError
1246
+ # @rbs (TCPSocket socket) -> void
1247
+ def deregister(socket)
1248
+ waiter = @waiters.delete(socket)
1249
+ waiter[0].transfer if waiter
1223
1250
  end
1224
1251
 
1225
- # Registers a socket with the NIO selector and sets up timeout tracking.
1252
+ # Registers a socket for readiness and sets up timeout tracking.
1226
1253
  #
1227
1254
  # @param socket [TCPSocket] the socket to register
1228
1255
  # @return [void]
@@ -1235,9 +1262,9 @@ module Raptor
1235
1262
  if state[:protocol] == :http2
1236
1263
  io = @id_to_io[state[:id]]
1237
1264
  io.reading = true
1238
- update_monitor(state[:id], io)
1265
+ update_interests(state[:id], io)
1239
1266
  else
1240
- @selector.register(socket, :r).value = socket
1267
+ wait_for_io(socket, IO::READABLE)
1241
1268
  end
1242
1269
 
1243
1270
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -1301,17 +1328,17 @@ module Raptor
1301
1328
  # Dispatches readable and writable events without allowing application
1302
1329
  # threads to take ownership of HTTP/2 sockets.
1303
1330
  #
1304
- # @param monitor [NIO::Monitor] ready selector monitor
1331
+ # @param socket [TCPSocket] ready client socket
1332
+ # @param events [Integer] readiness events
1305
1333
  # @return [void]
1306
1334
  #
1307
- # @rbs (NIO::Monitor monitor) -> void
1308
- def handle_monitor(monitor)
1309
- socket = monitor.value
1335
+ # @rbs (TCPSocket socket, Integer events) -> void
1336
+ def handle_ready(socket, events)
1310
1337
  state = @socket_to_state[socket]
1311
1338
  return unless state
1312
1339
 
1313
1340
  if state[:detached]
1314
- handle_http1_detached_monitor(monitor, state[:id])
1341
+ handle_http1_detached_ready(socket, events, state[:id])
1315
1342
  return
1316
1343
  end
1317
1344
 
@@ -1325,7 +1352,7 @@ module Raptor
1325
1352
  return unless io
1326
1353
 
1327
1354
  wait = io.wait
1328
- if (wait == :read && monitor.readable?) || (wait == :write && monitor.writable?)
1355
+ if (wait == :read && events.anybits?(IO::READABLE)) || (wait == :write && events.anybits?(IO::WRITABLE))
1329
1356
  flush_output(id, io)
1330
1357
  return unless @id_to_socket.key?(id)
1331
1358
  return if io.wait == :read
@@ -1333,40 +1360,43 @@ module Raptor
1333
1360
  flush_http2_detached_bodies(id) unless io.detached.empty?
1334
1361
  end
1335
1362
 
1336
- return unless io.reading && monitor.readable?
1363
+ return unless io.reading && events.anybits?(IO::READABLE)
1337
1364
 
1338
1365
  io.reading = false
1339
- update_monitor(id, io)
1366
+ update_interests(id, io)
1340
1367
  client = @id_to_timeout.delete(id)
1341
1368
  @timeouts.delete!(client) if client
1342
1369
  read_and_queue_for_parse(socket, state)
1370
+ ensure
1371
+ io = @id_to_io[state[:id]] if state
1372
+ update_interests(state[:id], io) if io
1343
1373
  end
1344
1374
 
1345
1375
  # Handles readiness for an HTTP/1.x detached response.
1346
1376
  #
1347
- # @param monitor [NIO::Monitor] ready selector monitor
1377
+ # @param socket [TCPSocket] ready client socket
1378
+ # @param events [Integer] readiness events
1348
1379
  # @param id [Integer] unique connection identifier
1349
1380
  # @return [void]
1350
1381
  #
1351
- # @rbs (NIO::Monitor monitor, Integer id) -> void
1352
- def handle_http1_detached_monitor(monitor, id)
1353
- socket = monitor.value
1382
+ # @rbs (TCPSocket socket, Integer events, Integer id) -> void
1383
+ def handle_http1_detached_ready(socket, events, id)
1354
1384
  io = @id_to_io[id]
1355
1385
  return unless io.is_a?(Http1IO)
1356
1386
 
1357
1387
  wait = io.wait
1358
- if (wait == :read && monitor.readable?) || (wait == :write && monitor.writable?)
1388
+ if (wait == :read && events.anybits?(IO::READABLE)) || (wait == :write && events.anybits?(IO::WRITABLE))
1359
1389
  flush_output(id, io)
1360
1390
  return unless @id_to_io.key?(id)
1361
1391
 
1362
1392
  flush_http1_detached_body(id) unless io.wait
1363
1393
  end
1364
1394
 
1365
- return unless io.reading && monitor.readable?
1395
+ return unless io.reading && events.anybits?(IO::READABLE)
1366
1396
 
1367
1397
  io.input << socket.read_nonblock(CHUNK_SIZE)
1368
1398
  io.reading = false
1369
- update_monitor(id, io)
1399
+ update_interests(id, io)
1370
1400
  rescue IO::WaitReadable
1371
1401
  rescue IO::WaitWritable
1372
1402
  wait_for_write(id, io, :write)
@@ -1382,7 +1412,7 @@ module Raptor
1382
1412
  #
1383
1413
  # @rbs (TCPSocket socket) -> void
1384
1414
  def wakeup!(socket)
1385
- @selector.deregister(socket)
1415
+ deregister(socket)
1386
1416
  state = @socket_to_state[socket]
1387
1417
  client = @id_to_timeout.delete(state[:id])
1388
1418
  @timeouts.delete!(client) if client
@@ -1433,7 +1463,7 @@ module Raptor
1433
1463
  socket.read_nonblock(CHUNK_SIZE)
1434
1464
  rescue IO::WaitReadable
1435
1465
  @queue << socket
1436
- @selector.wakeup
1466
+ wakeup
1437
1467
  return
1438
1468
  rescue EOFError, IOError, SystemCallError, OpenSSL::SSL::SSLError
1439
1469
  cleanup(socket)
@@ -1461,7 +1491,7 @@ module Raptor
1461
1491
  def cleanup(socket)
1462
1492
  state = @socket_to_state.delete(socket)
1463
1493
  io = @id_to_io.delete(state[:id])
1464
- deregister(socket, io)
1494
+ deregister(socket)
1465
1495
  @id_to_socket.delete(state[:id])
1466
1496
  @id_to_writer.delete(state[:id])
1467
1497
  @id_to_flow_control.delete(state[:id])&.close
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Raptor
5
- VERSION = "0.23.0"
5
+ VERSION = "0.25.0"
6
6
  end
data/lib/raptor.rb CHANGED
@@ -8,7 +8,7 @@ require_relative "raptor/detached_body"
8
8
  #
9
9
  # Raptor is a high-performance, preloading, multi-process, multi-threaded Ruby 4+ web server
10
10
  # implementing Rack 3+, leveraging Ractors for parallel HTTP/1.1 and HTTP/2 request processing,
11
- # native C extensions for HTTP parsing and HPACK compression, and NIO for non-blocking I/O.
11
+ # native C extensions for HTTP parsing and HPACK compression, and io-event for non-blocking I/O.
12
12
  #
13
13
  module Raptor
14
14
  end
@@ -66,6 +66,8 @@ module Raptor
66
66
 
67
67
  @binder: Binder
68
68
 
69
+ @plugins: Array[Plugin]
70
+
69
71
  @before_refork: Array[Proc]
70
72
 
71
73
  @before_worker_shutdown: Array[Proc]
@@ -152,6 +154,11 @@ module Raptor
152
154
 
153
155
  @drain_accept_queue: bool
154
156
 
157
+ # Returns the cluster configuration available to plugins.
158
+ #
159
+ # @return [Hash{Symbol => untyped}]
160
+ attr_reader options: Hash[Symbol, untyped]
161
+
155
162
  # Creates a new Cluster with the given options.
156
163
  #
157
164
  # @param options [Hash] cluster configuration options
@@ -180,6 +187,7 @@ module Raptor
180
187
  # @option options [Array<Proc>] :before_worker_boot procs called with the worker index before it begins serving
181
188
  # @option options [Array<Proc>] :before_worker_shutdown procs called with the worker index before its graceful shutdown
182
189
  # @option options [Array<Proc>] :before_refork procs called in a worker before it transitions to a seed
190
+ # @option options [Array<String, Symbol>] :plugins plugins to load and start in order
183
191
  # @option options [String, nil] :stats_file path to write per-worker stats JSON, or nil to disable
184
192
  # @option options [String, nil] :control_url `unix://` URL serving cluster stats, or nil to disable
185
193
  # @option options [String, nil] :pid_file path to write the master PID to, or nil to disable
@@ -210,17 +218,22 @@ module Raptor
210
218
  # @rbs () -> Array[Hash[Symbol, untyped]]
211
219
  def stats: () -> Array[Hash[Symbol, untyped]]
212
220
 
213
- private
214
-
215
- # Returns cluster statistics for the control server. For an adaptive
216
- # pool, `max_threads` is the number of threads currently running so
217
- # utilization reflects the capacity available at that moment.
221
+ # Returns cluster statistics for plugins and the control server.
222
+ # For an adaptive pool, `max_threads` is the number of threads currently
223
+ # running so utilization reflects the capacity available at that moment.
218
224
  #
219
225
  # @return [Hash] cluster statistics
220
226
  #
221
227
  # @rbs () -> Hash[Symbol, untyped]
222
228
  def stats_hash: () -> Hash[Symbol, untyped]
223
229
 
230
+ private
231
+
232
+ # Stops plugins in reverse startup order.
233
+ #
234
+ # @rbs () -> void
235
+ def stop_plugins: () -> void
236
+
224
237
  # @rbs (Numeric time) -> String
225
238
  def timestamp: (Numeric time) -> String
226
239
 
@@ -11,8 +11,6 @@ module Raptor
11
11
 
12
12
  CONTENT_TYPE: ::String
13
13
 
14
- HTTP_VERSION: ::String
15
-
16
14
  REMOTE_ADDR: ::String
17
15
 
18
16
  REQUEST_URI: ::String
@@ -34,6 +34,8 @@ module Raptor
34
34
 
35
35
  INTERNAL_SERVER_ERROR_RESPONSE: ::String
36
36
 
37
+ NOT_IMPLEMENTED_RESPONSE: ::String
38
+
37
39
  CHUNKED_TERMINATOR: ::String
38
40
 
39
41
  CONNECTION_CLOSE: ::String
@@ -72,17 +74,14 @@ module Raptor
72
74
  # @rbs (Hash[String, untyped] env) -> bool
73
75
  def self.invalid_host?: (Hash[String, untyped] env) -> bool
74
76
 
75
- # Returns true when the message framing shows a request-smuggling vector
76
- # per RFC 9112 section 6.3: a `Transfer-Encoding` where `chunked` is
77
- # missing, not the final encoding, or duplicated; a `Transfer-Encoding`
78
- # paired with a `Content-Length`; or a `Content-Length` containing any
79
- # non-digit character.
77
+ # Rejects ambiguous message framing per RFC 9112 section 6.3 and
78
+ # transfer codings other than a single `chunked`.
80
79
  #
81
80
  # @param env [Hash] the Rack environment after header parsing
82
- # @return [Boolean]
81
+ # @return [Symbol, nil] `:malformed`, `:unsupported_encoding`, or nil
83
82
  #
84
- # @rbs (Hash[String, untyped] env) -> bool
85
- def self.request_smuggling?: (Hash[String, untyped] env) -> bool
83
+ # @rbs (Hash[String, untyped] env) -> Symbol?
84
+ def self.framing_error: (Hash[String, untyped] env) -> Symbol?
86
85
 
87
86
  # Returns any pipelined bytes following the request, or nil if none remain.
88
87
  #
@@ -112,9 +111,9 @@ module Raptor
112
111
  # Advances an HTTP/1.x request parse from the state hash's buffered
113
112
  # bytes. A complete well-formed request returns with `:complete` set
114
113
  # plus populated `:env` and `:body`, and records any bytes after it in
115
- # the parse data's `:pipelined`; malformed or oversized input flips
116
- # `:malformed` or `:too_large`; incomplete input returns the state
117
- # so the reactor can wait for more.
114
+ # the parse data's `:pipelined`. Rejected input sets `:malformed`,
115
+ # `:too_large`, or `:unsupported_encoding`; incomplete input returns
116
+ # the state so the reactor can wait for more.
118
117
  #
119
118
  # @param data [Hash] the current parse state
120
119
  # @param env_template [Hash] the Rack env template to seed the request with
@@ -256,7 +255,7 @@ module Raptor
256
255
  # @param parser [HttpParser] the parser holding the finished parse state
257
256
  # @param nread [Integer] the byte offset where the body begins in `buffer`
258
257
  # @return [String, nil, Symbol] body, nil when absent, or `:incomplete`,
259
- # `:too_large`, or `:malformed`
258
+ # `:too_large`, `:malformed`, or `:unsupported_encoding`
260
259
  #
261
260
  # @rbs (String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, HttpParser parser, Integer nread) -> (String | Symbol)?
262
261
  def extract_body: (String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, HttpParser parser, Integer nread) -> (String | Symbol)?
@@ -384,6 +383,14 @@ module Raptor
384
383
  # @rbs (TCPSocket socket) -> void
385
384
  def reject_malformed: (TCPSocket socket) -> void
386
385
 
386
+ # Writes a 501 response and closes the socket.
387
+ #
388
+ # @param socket [TCPSocket] the client socket
389
+ # @return [void]
390
+ #
391
+ # @rbs (TCPSocket socket) -> void
392
+ def reject_unsupported_encoding: (TCPSocket socket) -> void
393
+
387
394
  # Builds a Rack environment hash from parsed HTTP request data.
388
395
  #
389
396
  # @param env [Hash] partial env hash from the HTTP parser
@@ -399,7 +406,8 @@ module Raptor
399
406
 
400
407
  # Builds the `rack.input` IO object for the request body. Returns an
401
408
  # in-memory StringIO for bodies up to the spool threshold, or a Tempfile
402
- # for larger bodies to bound per-worker memory.
409
+ # for larger bodies. The complete body is already buffered at this point,
410
+ # so spooling does not bound peak receive memory.
403
411
  #
404
412
  # @param body [String, nil] decoded request body
405
413
  # @return [IO] an IO-like object positioned at the start of the body
@@ -307,7 +307,7 @@ module Raptor
307
307
  # updated connection state along with any outgoing protocol frames and
308
308
  # completed stream requests.
309
309
  #
310
- # @param data [Hash] the connection state including buffer and HPACK table
310
+ # @param data [Hash] the connection state including buffer, HPACK table and body-size limit
311
311
  # @return [Hash] updated state with outgoing_frames and completed_requests
312
312
  #
313
313
  # @rbs (Hash[Symbol, untyped] data) -> Hash[Symbol, untyped]
@@ -381,23 +381,25 @@ module Raptor
381
381
  # @rbs (Hash[Symbol, untyped] data, String buffer, Array[untyped] hpack_table, Hash[Integer, Hash[Symbol, untyped]] streams, Array[String] outgoing_frames, Array[Hash[Symbol, untyped]] completed_requests, Array[Integer] cancelled_streams, Array[[Integer, Integer]] window_updates, Array[String] ping_acknowledgements, Integer? peer_initial_window_size, Integer connection_window, bool preface_received, Integer last_client_stream_id, Hash[Symbol, untyped]? pending_headers, bool close_connection) -> Hash[Symbol, untyped]
382
382
  def self.build_result: (Hash[Symbol, untyped] data, String buffer, Array[untyped] hpack_table, Hash[Integer, Hash[Symbol, untyped]] streams, Array[String] outgoing_frames, Array[Hash[Symbol, untyped]] completed_requests, Array[Integer] cancelled_streams, Array[[ Integer, Integer ]] window_updates, Array[String] ping_acknowledgements, Integer? peer_initial_window_size, Integer connection_window, bool preface_received, Integer last_client_stream_id, Hash[Symbol, untyped]? pending_headers, bool close_connection) -> Hash[Symbol, untyped]
383
383
 
384
- @initial_settings_frame: String
384
+ @app: ^(Hash[String, untyped]) -> [ Integer, Hash[String, String | Array[String]], untyped ]
385
385
 
386
- @running: AtomicBoolean
386
+ @server_port_string: String
387
387
 
388
- @on_error: (^(Hash[String, untyped]?, Exception) -> void)?
388
+ @write_timeout: Integer
389
389
 
390
- @clean_fiber_locals: bool
390
+ @max_body_size: Integer?
391
+
392
+ @access_log_io: IO?
391
393
 
392
394
  @clean_thread_locals: bool
393
395
 
394
- @access_log_io: IO?
396
+ @clean_fiber_locals: bool
395
397
 
396
- @write_timeout: Integer
398
+ @on_error: (^(Hash[String, untyped]?, Exception) -> void)?
397
399
 
398
- @server_port_string: String
400
+ @running: AtomicBoolean
399
401
 
400
- @app: ^(Hash[String, untyped]) -> [ Integer, Hash[String, String | Array[String]], untyped ]
402
+ @initial_settings_frame: String
401
403
 
402
404
  # Creates a new Http2 handler.
403
405
  #
@@ -405,6 +407,7 @@ module Raptor
405
407
  # @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
406
408
  # @param connection_options [Hash] per-connection settings shared across protocols
407
409
  # @option connection_options [Integer] :write_timeout per-write socket timeout in seconds
410
+ # @option connection_options [Integer, nil] :max_body_size maximum request body size in bytes
408
411
  # @param http2_options [Hash] HTTP/2-specific settings
409
412
  # @option http2_options [Integer] :max_concurrent_streams maximum HTTP/2 concurrent streams per connection
410
413
  # @param access_log_io [IO, nil] IO to write Common Log Format access entries to, or nil to disable