quicsilver 0.3.0 → 0.4.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/.github/workflows/ci.yml +1 -1
- data/.github/workflows/cibuildgem.yaml +93 -0
- data/.gitignore +3 -1
- data/CHANGELOG.md +32 -0
- data/Gemfile.lock +20 -2
- data/README.md +92 -29
- data/Rakefile +67 -2
- data/benchmarks/concurrent.rb +2 -2
- data/benchmarks/rails.rb +3 -3
- data/benchmarks/throughput.rb +2 -2
- data/examples/README.md +44 -91
- data/examples/benchmark.rb +111 -0
- data/examples/connection_pool_demo.rb +47 -0
- data/examples/example_helper.rb +18 -0
- data/examples/falcon_middleware.rb +44 -0
- data/examples/feature_demo.rb +125 -0
- data/examples/grpc_style.rb +97 -0
- data/examples/minimal_http3_server.rb +6 -18
- data/examples/priorities.rb +60 -0
- data/examples/protocol_http_server.rb +31 -0
- data/examples/rack_http3_server.rb +8 -20
- data/examples/rails_feature_test.rb +260 -0
- data/examples/simple_client_test.rb +2 -2
- data/examples/streaming_sse.rb +33 -0
- data/examples/trailers.rb +69 -0
- data/ext/quicsilver/extconf.rb +14 -0
- data/ext/quicsilver/quicsilver.c +39 -0
- data/lib/quicsilver/client/client.rb +138 -39
- data/lib/quicsilver/client/connection_pool.rb +106 -0
- data/lib/quicsilver/libmsquic.2.dylib +0 -0
- data/lib/quicsilver/protocol/adapter.rb +176 -0
- data/lib/quicsilver/protocol/control_stream_parser.rb +106 -0
- data/lib/quicsilver/protocol/frame_parser.rb +142 -0
- data/lib/quicsilver/protocol/frame_reader.rb +55 -0
- data/lib/quicsilver/protocol/frames.rb +18 -7
- data/lib/quicsilver/protocol/priority.rb +56 -0
- data/lib/quicsilver/protocol/qpack/encoder.rb +39 -1
- data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +16 -1
- data/lib/quicsilver/protocol/request_parser.rb +28 -140
- data/lib/quicsilver/protocol/response_encoder.rb +27 -2
- data/lib/quicsilver/protocol/response_parser.rb +22 -130
- data/lib/quicsilver/protocol/stream_input.rb +98 -0
- data/lib/quicsilver/protocol/stream_output.rb +59 -0
- data/lib/quicsilver/quicsilver.bundle +0 -0
- data/lib/quicsilver/server/request_handler.rb +96 -44
- data/lib/quicsilver/server/server.rb +316 -42
- data/lib/quicsilver/transport/configuration.rb +10 -1
- data/lib/quicsilver/transport/connection.rb +92 -63
- data/lib/quicsilver/version.rb +1 -1
- data/lib/quicsilver.rb +26 -3
- data/quicsilver.gemspec +10 -2
- metadata +69 -5
- data/examples/setup_certs.sh +0 -57
|
@@ -15,6 +15,28 @@ module Quicsilver
|
|
|
15
15
|
ServerStopError = Class.new(StandardError)
|
|
16
16
|
DrainTimeoutError = Class.new(StandardError)
|
|
17
17
|
|
|
18
|
+
# Tracks an in-flight streaming request between RECEIVE and RECEIVE_FIN.
|
|
19
|
+
# The stream handle arrives at RECEIVE_FIN; the worker thread waits for it.
|
|
20
|
+
PendingStream = Struct.new(:connection, :body, :request, :stream_id, :stream_handle, :handle_ready, :frame_buffer, :priority, keyword_init: true) do
|
|
21
|
+
def initialize(**)
|
|
22
|
+
super
|
|
23
|
+
self.handle_ready = Queue.new
|
|
24
|
+
self.frame_buffer = "".b
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Called by RECEIVE_FIN handler to provide the stream handle
|
|
28
|
+
def complete(handle)
|
|
29
|
+
self.stream_handle = handle
|
|
30
|
+
handle_ready.push(true)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Called by worker thread to wait for the stream handle
|
|
34
|
+
def wait_for_handle(timeout: 30)
|
|
35
|
+
handle_ready.pop(timeout: timeout)
|
|
36
|
+
stream_handle
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
18
40
|
class << self
|
|
19
41
|
attr_accessor :instance
|
|
20
42
|
|
|
@@ -47,9 +69,13 @@ module Quicsilver
|
|
|
47
69
|
@max_connections = max_connections
|
|
48
70
|
@cancelled_streams = Set.new
|
|
49
71
|
@cancelled_mutex = Mutex.new
|
|
72
|
+
@pending_streams = {} # stream_id => PendingStream (for streaming dispatch)
|
|
73
|
+
@pending_mutex = Mutex.new
|
|
74
|
+
|
|
75
|
+
protocol_app = wrap_app(@app, @server_configuration.mode)
|
|
50
76
|
|
|
51
77
|
@request_handler = RequestHandler.new(
|
|
52
|
-
app:
|
|
78
|
+
app: protocol_app,
|
|
53
79
|
configuration: @server_configuration,
|
|
54
80
|
request_registry: @request_registry,
|
|
55
81
|
cancelled_streams: @cancelled_streams,
|
|
@@ -164,8 +190,15 @@ module Quicsilver
|
|
|
164
190
|
# Phase 2: Drain in-flight requests
|
|
165
191
|
drain(timeout: timeout)
|
|
166
192
|
|
|
193
|
+
# Phase 2b: Send final GOAWAY with actual last processed stream ID (RFC 9114 §5.2)
|
|
194
|
+
@connections.each_value do |c|
|
|
195
|
+
c.send_goaway
|
|
196
|
+
rescue => e
|
|
197
|
+
Quicsilver.logger.debug("Second GOAWAY failed: #{e.message}")
|
|
198
|
+
end
|
|
199
|
+
|
|
167
200
|
# Grace period: let pending responses reach clients
|
|
168
|
-
sleep 0.5
|
|
201
|
+
sleep [0.5, timeout * 0.1].min
|
|
169
202
|
|
|
170
203
|
# Log any requests that didn't complete
|
|
171
204
|
unless @request_registry.empty?
|
|
@@ -177,7 +210,7 @@ module Quicsilver
|
|
|
177
210
|
|
|
178
211
|
# Phase 3: Shutdown connections
|
|
179
212
|
@connections.each_value(&:shutdown)
|
|
180
|
-
sleep 0.1
|
|
213
|
+
sleep [0.1, timeout * 0.05].min
|
|
181
214
|
|
|
182
215
|
# Phase 4: Hard stop
|
|
183
216
|
stop
|
|
@@ -197,7 +230,8 @@ module Quicsilver
|
|
|
197
230
|
return
|
|
198
231
|
end
|
|
199
232
|
|
|
200
|
-
connection = Transport::Connection.new(connection_handle, connection_data
|
|
233
|
+
connection = Transport::Connection.new(connection_handle, connection_data,
|
|
234
|
+
max_header_size: @server_configuration.max_header_size)
|
|
201
235
|
@connections[connection_handle] = connection
|
|
202
236
|
connection.setup_http3_streams
|
|
203
237
|
|
|
@@ -207,45 +241,12 @@ module Quicsilver
|
|
|
207
241
|
|
|
208
242
|
when STREAM_EVENT_SEND_COMPLETE
|
|
209
243
|
# Buffer cleanup handled in C extension
|
|
210
|
-
|
|
211
244
|
when STREAM_EVENT_RECEIVE
|
|
212
245
|
return unless (connection = @connections[connection_handle])
|
|
213
|
-
|
|
214
|
-
# Unidirectional streams (control, QPACK) must be processed incrementally —
|
|
215
|
-
# they never send FIN, so waiting for RECEIVE_FIN would mean never parsing.
|
|
216
|
-
if (stream_id & 0x02) != 0 # unidirectional
|
|
217
|
-
begin
|
|
218
|
-
connection.receive_unidirectional_data(stream_id, data)
|
|
219
|
-
rescue Protocol::FrameError => e
|
|
220
|
-
Quicsilver.logger.error("Control stream error: #{e.message} (0x#{e.error_code.to_s(16)})")
|
|
221
|
-
Quicsilver.connection_shutdown(connection_handle, e.error_code, false) rescue nil
|
|
222
|
-
end
|
|
223
|
-
else
|
|
224
|
-
connection.buffer_data(stream_id, data)
|
|
225
|
-
end
|
|
226
|
-
|
|
246
|
+
handle_receive(connection, connection_handle, stream_id, data, early_data: early_data)
|
|
227
247
|
when STREAM_EVENT_RECEIVE_FIN
|
|
228
248
|
return unless (connection = @connections[connection_handle])
|
|
229
|
-
|
|
230
|
-
event = Transport::StreamEvent.new(data, "RECEIVE_FIN")
|
|
231
|
-
|
|
232
|
-
full_data = connection.complete_stream(stream_id, event.data)
|
|
233
|
-
stream = Transport::InboundStream.new(stream_id)
|
|
234
|
-
stream.stream_handle = event.handle
|
|
235
|
-
stream.append_data(full_data)
|
|
236
|
-
|
|
237
|
-
if stream.bidirectional?
|
|
238
|
-
connection.track_client_stream(stream_id)
|
|
239
|
-
dispatch_request(connection, stream, early_data: early_data)
|
|
240
|
-
else
|
|
241
|
-
begin
|
|
242
|
-
connection.handle_unidirectional_stream(stream)
|
|
243
|
-
rescue Protocol::FrameError => e
|
|
244
|
-
Quicsilver.logger.error("Control stream error: #{e.message} (0x#{e.error_code.to_s(16)})")
|
|
245
|
-
Quicsilver.connection_shutdown(connection_handle, e.error_code, false) rescue nil
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
+
handle_receive_fin(connection, connection_handle, stream_id, data, early_data: early_data)
|
|
249
250
|
when STREAM_EVENT_STREAM_RESET
|
|
250
251
|
return unless (connection = @connections[connection_handle])
|
|
251
252
|
event = Transport::StreamEvent.new(data, "STREAM_RESET")
|
|
@@ -257,9 +258,10 @@ module Quicsilver
|
|
|
257
258
|
Quicsilver.connection_shutdown(connection_handle, Protocol::H3_CLOSED_CRITICAL_STREAM, false) rescue nil
|
|
258
259
|
else
|
|
259
260
|
@cancelled_mutex.synchronize { @cancelled_streams.add(stream_id) }
|
|
261
|
+
pending = @pending_mutex.synchronize { @pending_streams.delete(stream_id) }
|
|
262
|
+
pending&.body&.close(RuntimeError.new("Stream #{stream_id} reset by peer"))
|
|
260
263
|
@request_registry.complete(stream_id)
|
|
261
264
|
end
|
|
262
|
-
|
|
263
265
|
when STREAM_EVENT_STOP_SENDING
|
|
264
266
|
return unless @connections[connection_handle]
|
|
265
267
|
event = Transport::StreamEvent.new(data, "STOP_SENDING")
|
|
@@ -272,6 +274,30 @@ module Quicsilver
|
|
|
272
274
|
|
|
273
275
|
private
|
|
274
276
|
|
|
277
|
+
# Wrap the user's app for the configured mode.
|
|
278
|
+
# Rack mode: inject rack.early_hints support, then wrap with protocol-rack.
|
|
279
|
+
# Falcon mode: pass through as-is (native protocol-http app).
|
|
280
|
+
def wrap_app(app, mode)
|
|
281
|
+
case mode
|
|
282
|
+
when :falcon then app
|
|
283
|
+
else ::Protocol::Rack::Adapter.new(with_early_hints(app))
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Bridges protocol-http's interim_response to Rack's rack.early_hints.
|
|
288
|
+
# In a Rails controller: send_early_hints("link" => '</style.css>; rel=preload')
|
|
289
|
+
def with_early_hints(app)
|
|
290
|
+
->(env) {
|
|
291
|
+
request = env["protocol.http.request"]
|
|
292
|
+
if request&.respond_to?(:interim_response) && request.interim_response
|
|
293
|
+
env["rack.early_hints"] = ->(headers) {
|
|
294
|
+
request.send_interim_response(103, ::Protocol::HTTP::Headers[headers.map { |k, v| [k, v] }])
|
|
295
|
+
}
|
|
296
|
+
end
|
|
297
|
+
app.call(env)
|
|
298
|
+
}
|
|
299
|
+
end
|
|
300
|
+
|
|
275
301
|
def setup_signal_handlers
|
|
276
302
|
%w[INT TERM].each do |signal|
|
|
277
303
|
trap(signal) { Thread.new { shutdown } }
|
|
@@ -301,6 +327,74 @@ module Quicsilver
|
|
|
301
327
|
|
|
302
328
|
attr_reader :work_queue
|
|
303
329
|
|
|
330
|
+
def handle_receive(connection, connection_handle, stream_id, data, early_data: false)
|
|
331
|
+
# Unidirectional streams (control, QPACK) must be processed incrementally —
|
|
332
|
+
# they never send FIN, so waiting for RECEIVE_FIN would mean never parsing.
|
|
333
|
+
if (stream_id & 0x02) != 0 # unidirectional
|
|
334
|
+
begin
|
|
335
|
+
connection.receive_unidirectional_data(stream_id, data)
|
|
336
|
+
rescue Protocol::FrameError => e
|
|
337
|
+
Quicsilver.logger.error("Control stream error: #{e.message} (0x#{e.error_code.to_s(16)})")
|
|
338
|
+
Quicsilver.connection_shutdown(connection_handle, e.error_code, false) rescue nil
|
|
339
|
+
end
|
|
340
|
+
else
|
|
341
|
+
handle_bidi_receive(connection, connection_handle, stream_id, data, early_data: early_data)
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def handle_bidi_receive(connection, connection_handle, stream_id, data, early_data: false)
|
|
346
|
+
pending = @pending_mutex.synchronize { @pending_streams[stream_id] }
|
|
347
|
+
if pending
|
|
348
|
+
# Subsequent RECEIVE — append to frame buffer and extract complete DATA payloads.
|
|
349
|
+
# MsQuic splits data at arbitrary boundaries, so frames may span callbacks.
|
|
350
|
+
pending.frame_buffer << data
|
|
351
|
+
drain_data_frames(pending)
|
|
352
|
+
elsif contains_headers_frame?(data)
|
|
353
|
+
dispatch_streaming(connection, connection_handle, stream_id, data, early_data: early_data)
|
|
354
|
+
else
|
|
355
|
+
connection.buffer_data(stream_id, data)
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def handle_receive_fin(connection, connection_handle, stream_id, data, early_data: false)
|
|
360
|
+
event = Transport::StreamEvent.new(data, "RECEIVE_FIN")
|
|
361
|
+
|
|
362
|
+
pending = @pending_mutex.synchronize { @pending_streams[stream_id] }
|
|
363
|
+
if pending
|
|
364
|
+
complete_streaming_request(pending, event)
|
|
365
|
+
else
|
|
366
|
+
complete_buffered_request(connection, connection_handle, stream_id, event, early_data: early_data)
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def complete_streaming_request(pending, event)
|
|
371
|
+
if event.data && !event.data.empty?
|
|
372
|
+
pending.frame_buffer << event.data
|
|
373
|
+
drain_data_frames(pending)
|
|
374
|
+
end
|
|
375
|
+
pending.body.close_write
|
|
376
|
+
pending.complete(event.handle)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def complete_buffered_request(connection, connection_handle, stream_id, event, early_data: false)
|
|
380
|
+
full_data = connection.complete_stream(stream_id, event.data)
|
|
381
|
+
stream = Transport::InboundStream.new(stream_id)
|
|
382
|
+
stream.stream_handle = event.handle
|
|
383
|
+
stream.append_data(full_data)
|
|
384
|
+
|
|
385
|
+
if stream.bidirectional?
|
|
386
|
+
connection.track_client_stream(stream_id)
|
|
387
|
+
dispatch_request(connection, stream, early_data: early_data)
|
|
388
|
+
else
|
|
389
|
+
begin
|
|
390
|
+
connection.handle_unidirectional_stream(stream)
|
|
391
|
+
rescue Protocol::FrameError => e
|
|
392
|
+
Quicsilver.logger.error("Control stream error: #{e.message} (0x#{e.error_code.to_s(16)})")
|
|
393
|
+
Quicsilver.connection_shutdown(connection_handle, e.error_code, false) rescue nil
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
304
398
|
def dispatch_request(connection, stream, early_data: false)
|
|
305
399
|
if @work_queue.size >= @max_queue_size
|
|
306
400
|
Quicsilver.logger.warn("Work queue full (#{@max_queue_size}), rejecting request")
|
|
@@ -315,14 +409,194 @@ module Quicsilver
|
|
|
315
409
|
thread = Thread.new do
|
|
316
410
|
while (work = @work_queue.pop)
|
|
317
411
|
break if work == :shutdown
|
|
318
|
-
|
|
319
|
-
|
|
412
|
+
|
|
413
|
+
if work.is_a?(Array) && work[0] == :streaming
|
|
414
|
+
handle_streaming_request(work[1])
|
|
415
|
+
else
|
|
416
|
+
connection, stream, early_data = work
|
|
417
|
+
@request_handler.call(connection, stream, early_data: early_data)
|
|
418
|
+
end
|
|
320
419
|
end
|
|
321
420
|
end
|
|
322
421
|
@handler_mutex.synchronize { @handler_threads << thread }
|
|
323
422
|
end
|
|
324
423
|
end
|
|
325
424
|
|
|
425
|
+
# Streaming dispatch: parse headers from first RECEIVE, dispatch immediately.
|
|
426
|
+
# Body data arrives via subsequent RECEIVE events into StreamInput.
|
|
427
|
+
def dispatch_streaming(connection, connection_handle, stream_id, data, early_data: false)
|
|
428
|
+
parser = Protocol::RequestParser.new(
|
|
429
|
+
data,
|
|
430
|
+
max_header_size: @server_configuration.max_header_size,
|
|
431
|
+
max_header_count: @server_configuration.max_header_count,
|
|
432
|
+
max_frame_payload_size: @server_configuration.max_frame_payload_size
|
|
433
|
+
)
|
|
434
|
+
parser.parse
|
|
435
|
+
parser.validate_headers!
|
|
436
|
+
|
|
437
|
+
headers = parser.headers
|
|
438
|
+
return if headers.empty?
|
|
439
|
+
|
|
440
|
+
method = headers[":method"]
|
|
441
|
+
|
|
442
|
+
if @server_configuration.early_data_policy == :reject &&
|
|
443
|
+
early_data && !RequestHandler::SAFE_METHODS.include?(method)
|
|
444
|
+
Quicsilver.logger.debug("Rejected 0-RTT #{method} on stream #{stream_id} (no stream handle to send 425)")
|
|
445
|
+
return
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
request, body = @request_handler.adapter.build_request(headers)
|
|
449
|
+
request.headers.add("quicsilver-early-data", early_data.to_s)
|
|
450
|
+
|
|
451
|
+
# Feed body data from the first RECEIVE.
|
|
452
|
+
# The parser consumed complete frames (HEADERS + any complete DATA frames).
|
|
453
|
+
if body
|
|
454
|
+
# Complete DATA frames the parser extracted
|
|
455
|
+
if parser.body && parser.body.size > 0
|
|
456
|
+
parser.body.rewind
|
|
457
|
+
body_data = parser.body.read
|
|
458
|
+
body.write(body_data) unless body_data.empty?
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
pending = PendingStream.new(
|
|
463
|
+
connection: connection,
|
|
464
|
+
body: body,
|
|
465
|
+
request: request,
|
|
466
|
+
stream_id: stream_id,
|
|
467
|
+
priority: parser.priority
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
# Unconsumed bytes go into the frame buffer for incremental parsing
|
|
471
|
+
remainder = data.byteslice(parser.bytes_consumed..-1)
|
|
472
|
+
if remainder && remainder.bytesize > 0
|
|
473
|
+
pending.frame_buffer << remainder
|
|
474
|
+
drain_data_frames(pending)
|
|
475
|
+
end
|
|
476
|
+
@pending_mutex.synchronize { @pending_streams[stream_id] = pending }
|
|
477
|
+
|
|
478
|
+
connection.track_client_stream(stream_id)
|
|
479
|
+
@request_registry.track(stream_id, connection_handle,
|
|
480
|
+
path: headers[":path"] || "/", method: method || "GET")
|
|
481
|
+
|
|
482
|
+
if @work_queue.size >= @max_queue_size
|
|
483
|
+
Quicsilver.logger.warn("Work queue full (#{@max_queue_size}), rejecting request")
|
|
484
|
+
body&.close
|
|
485
|
+
@pending_mutex.synchronize { @pending_streams.delete(stream_id) }
|
|
486
|
+
else
|
|
487
|
+
@work_queue.push([:streaming, pending])
|
|
488
|
+
end
|
|
489
|
+
rescue Protocol::FrameError => e
|
|
490
|
+
Quicsilver.logger.error("Frame error: #{e.message}")
|
|
491
|
+
Quicsilver.connection_shutdown(connection_handle, e.error_code, false) rescue nil
|
|
492
|
+
rescue Protocol::MessageError => e
|
|
493
|
+
Quicsilver.logger.error("Message error on stream #{stream_id}: #{e.message}")
|
|
494
|
+
rescue => e
|
|
495
|
+
Quicsilver.logger.error("Error in streaming dispatch: #{e.class} - #{e.message}")
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def handle_streaming_request(pending)
|
|
499
|
+
response = @request_handler.adapter.call(pending.request)
|
|
500
|
+
|
|
501
|
+
# Wait for RECEIVE_FIN to provide the stream handle
|
|
502
|
+
stream_handle = pending.wait_for_handle(timeout: 30)
|
|
503
|
+
unless stream_handle
|
|
504
|
+
Quicsilver.logger.error("Timed out waiting for stream handle on stream #{pending.stream_id}")
|
|
505
|
+
return
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
return if cancelled_stream?(pending.stream_id)
|
|
509
|
+
|
|
510
|
+
headers = response.headers
|
|
511
|
+
|
|
512
|
+
trailers = if headers.respond_to?(:trailer?) && headers.trailer?
|
|
513
|
+
trailer_hash = {}
|
|
514
|
+
headers.trailer.each { |name, value| trailer_hash[name] = value }
|
|
515
|
+
trailer_hash
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
response_headers = {}
|
|
519
|
+
if headers.respond_to?(:header)
|
|
520
|
+
headers.header.each { |name, value| response_headers[name] = value }
|
|
521
|
+
else
|
|
522
|
+
headers&.each { |name, value| response_headers[name] = value }
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
if !response_headers.key?("content-length") && response.body&.length
|
|
526
|
+
response_headers["content-length"] = response.body.length.to_s
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
stream = Transport::InboundStream.new(pending.stream_id)
|
|
530
|
+
stream.stream_handle = stream_handle
|
|
531
|
+
|
|
532
|
+
pending.connection.apply_stream_priority(stream, pending.priority)
|
|
533
|
+
pending.connection.send_response(stream, response.status, response_headers, response.body,
|
|
534
|
+
head_request: pending.request.method == "HEAD", trailers: trailers)
|
|
535
|
+
@request_registry.complete(pending.stream_id)
|
|
536
|
+
rescue => e
|
|
537
|
+
Quicsilver.logger.error("Streaming request error: #{e.class} - #{e.message}")
|
|
538
|
+
if pending.stream_handle
|
|
539
|
+
stream = Transport::InboundStream.new(pending.stream_id)
|
|
540
|
+
stream.stream_handle = pending.stream_handle
|
|
541
|
+
pending.connection.send_error(stream, 500, "Internal Server Error") if stream.writable?
|
|
542
|
+
end
|
|
543
|
+
ensure
|
|
544
|
+
@pending_mutex.synchronize { @pending_streams.delete(pending.stream_id) }
|
|
545
|
+
@request_registry.complete(pending.stream_id) if @request_registry.include?(pending.stream_id)
|
|
546
|
+
@cancelled_mutex.synchronize { @cancelled_streams.delete(pending.stream_id) }
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# Incrementally extract complete DATA frame payloads from the frame buffer.
|
|
550
|
+
# Handles MsQuic splitting frames across RECEIVE callbacks — partial frames
|
|
551
|
+
# remain in the buffer until the next callback completes them.
|
|
552
|
+
def drain_data_frames(pending)
|
|
553
|
+
buf = pending.frame_buffer
|
|
554
|
+
|
|
555
|
+
while buf.bytesize >= 2
|
|
556
|
+
type_byte = buf.getbyte(0)
|
|
557
|
+
if type_byte < 0x40
|
|
558
|
+
type = type_byte
|
|
559
|
+
type_len = 1
|
|
560
|
+
else
|
|
561
|
+
type, type_len = Protocol.decode_varint_str(buf, 0)
|
|
562
|
+
break if type_len == 0
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
len_byte = buf.getbyte(type_len)
|
|
566
|
+
break unless len_byte
|
|
567
|
+
if len_byte < 0x40
|
|
568
|
+
length = len_byte
|
|
569
|
+
length_len = 1
|
|
570
|
+
else
|
|
571
|
+
length, length_len = Protocol.decode_varint_str(buf, type_len)
|
|
572
|
+
break if length_len == 0
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
header_len = type_len + length_len
|
|
576
|
+
total = header_len + length
|
|
577
|
+
|
|
578
|
+
# Incomplete frame — wait for more data
|
|
579
|
+
break if buf.bytesize < total
|
|
580
|
+
|
|
581
|
+
if type == Protocol::FRAME_DATA
|
|
582
|
+
pending.body.write(buf.byteslice(header_len, length))
|
|
583
|
+
end
|
|
584
|
+
# Skip non-DATA frames (e.g. unknown extension frames)
|
|
585
|
+
|
|
586
|
+
buf = buf.byteslice(total..-1) || "".b
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
pending.frame_buffer = buf
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
# Heuristic: check if raw data starts with an HTTP/3 HEADERS frame (type 0x01).
|
|
593
|
+
# QUIC typically delivers complete frames, but if this misidentifies data,
|
|
594
|
+
# the parser will fail safely in dispatch_streaming's rescue handlers.
|
|
595
|
+
def contains_headers_frame?(data)
|
|
596
|
+
return false if data.nil? || data.bytesize < 2
|
|
597
|
+
data.getbyte(0) == Protocol::FRAME_HEADERS
|
|
598
|
+
end
|
|
599
|
+
|
|
326
600
|
def stop_worker_pool
|
|
327
601
|
@thread_pool_size.times { @work_queue.push(:shutdown) }
|
|
328
602
|
@handler_mutex.synchronize do
|
|
@@ -9,7 +9,8 @@ module Quicsilver
|
|
|
9
9
|
:keep_alive_interval_ms, :congestion_control_algorithm, :migration_enabled,
|
|
10
10
|
:disconnect_timeout_ms, :handshake_idle_timeout_ms,
|
|
11
11
|
:max_body_size, :max_header_size, :max_header_count, :max_frame_payload_size,
|
|
12
|
-
:early_data_policy
|
|
12
|
+
:early_data_policy,
|
|
13
|
+
:mode
|
|
13
14
|
|
|
14
15
|
QUIC_SERVER_RESUME_AND_ZERORTT = 1
|
|
15
16
|
QUIC_SERVER_RESUME_ONLY = 2
|
|
@@ -84,6 +85,14 @@ module Quicsilver
|
|
|
84
85
|
raise ServerConfigurationError, "Invalid early_data_policy: #{@early_data_policy.inspect} (must be :reject or :allow)"
|
|
85
86
|
end
|
|
86
87
|
|
|
88
|
+
# Application interface mode:
|
|
89
|
+
# :rack (default) — app is a Rack app, auto-wrapped with Protocol::Rack::Adapter
|
|
90
|
+
# :falcon — app is a native protocol-http app, used directly
|
|
91
|
+
@mode = options.fetch(:mode, :rack)
|
|
92
|
+
unless %i[rack falcon].include?(@mode)
|
|
93
|
+
raise ServerConfigurationError, "Invalid mode: #{@mode.inspect} (must be :rack or :falcon)"
|
|
94
|
+
end
|
|
95
|
+
|
|
87
96
|
@cert_file = cert_file.nil? ? DEFAULT_CERT_FILE : cert_file
|
|
88
97
|
@key_file = key_file.nil? ? DEFAULT_KEY_FILE : key_file
|
|
89
98
|
|