quicsilver 0.3.0 → 0.5.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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +9 -1
  3. data/.github/workflows/cibuildgem.yaml +117 -0
  4. data/.gitignore +8 -1
  5. data/CHANGELOG.md +82 -0
  6. data/Gemfile.lock +27 -2
  7. data/README.md +205 -29
  8. data/Rakefile +75 -3
  9. data/benchmarks/baseline.rb +175 -0
  10. data/benchmarks/compare.rb +398 -0
  11. data/benchmarks/helpers.rb +138 -64
  12. data/benchmarks/latency.rb +128 -0
  13. data/benchmarks/loss.rb +193 -0
  14. data/benchmarks/mobile.rb +125 -0
  15. data/docs/connection_identity.md +141 -0
  16. data/examples/README.md +44 -91
  17. data/examples/benchmark.rb +191 -0
  18. data/examples/connection_pool_demo.rb +47 -0
  19. data/examples/example_helper.rb +69 -0
  20. data/examples/falcon_middleware.rb +44 -0
  21. data/examples/feature_demo.rb +125 -0
  22. data/examples/grpc_style.rb +97 -0
  23. data/examples/latency_aware_routing.rb +382 -0
  24. data/examples/lifecycle_drain.rb +264 -0
  25. data/examples/minimal_http3_server.rb +6 -18
  26. data/examples/priorities.rb +60 -0
  27. data/examples/protocol_http_server.rb +31 -0
  28. data/examples/rack_http3_server.rb +8 -20
  29. data/examples/rails_feature_test.rb +280 -0
  30. data/examples/server_stats.rb +117 -0
  31. data/examples/simple_client_test.rb +5 -5
  32. data/examples/streaming_sse.rb +33 -0
  33. data/examples/trailers.rb +69 -0
  34. data/examples/webtransport_smoke.rb +546 -0
  35. data/examples/worker_load_balancing.rb +425 -0
  36. data/ext/quicsilver/extconf.rb +111 -12
  37. data/ext/quicsilver/include/msquic.h +2226 -0
  38. data/ext/quicsilver/include/msquic_posix.h +696 -0
  39. data/ext/quicsilver/include/quic_sal_stub.h +314 -0
  40. data/ext/quicsilver/msquic_patch.rb +68 -0
  41. data/ext/quicsilver/patches/reliable-reset.patch +723 -0
  42. data/ext/quicsilver/patches/stream-final-size.patch +47 -0
  43. data/ext/quicsilver/quicsilver.c +1646 -189
  44. data/lib/quicsilver/client/client.rb +546 -88
  45. data/lib/quicsilver/client/connection_pool.rb +202 -0
  46. data/lib/quicsilver/client/request.rb +106 -14
  47. data/lib/quicsilver/errors.rb +39 -0
  48. data/lib/quicsilver/libmsquic.2.dylib +0 -0
  49. data/lib/quicsilver/protocol/adapter.rb +196 -0
  50. data/lib/quicsilver/protocol/capsule.rb +38 -0
  51. data/lib/quicsilver/protocol/connect_stream_decoder.rb +75 -0
  52. data/lib/quicsilver/protocol/control_stream_parser.rb +117 -0
  53. data/lib/quicsilver/protocol/datagram.rb +40 -0
  54. data/lib/quicsilver/protocol/frame_parser.rb +141 -0
  55. data/lib/quicsilver/protocol/frame_reader.rb +84 -0
  56. data/lib/quicsilver/protocol/frames.rb +137 -10
  57. data/lib/quicsilver/protocol/priority.rb +65 -0
  58. data/lib/quicsilver/protocol/qpack/decoder.rb +4 -42
  59. data/lib/quicsilver/protocol/qpack/encoder.rb +49 -21
  60. data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +16 -1
  61. data/lib/quicsilver/protocol/qpack/huffman.rb +4 -4
  62. data/lib/quicsilver/protocol/request_encoder.rb +6 -3
  63. data/lib/quicsilver/protocol/request_parser.rb +43 -141
  64. data/lib/quicsilver/protocol/response_encoder.rb +40 -5
  65. data/lib/quicsilver/protocol/response_parser.rb +26 -128
  66. data/lib/quicsilver/protocol/stream_input.rb +98 -0
  67. data/lib/quicsilver/protocol/stream_output.rb +59 -0
  68. data/lib/quicsilver/protocol/unidirectional_stream.rb +12 -0
  69. data/lib/quicsilver/protocol/web_transport.rb +214 -0
  70. data/lib/quicsilver/rack/context.rb +33 -0
  71. data/lib/quicsilver/response.rb +40 -0
  72. data/lib/quicsilver/server/rack_adapter.rb +77 -0
  73. data/lib/quicsilver/server/receive_queue.rb +106 -0
  74. data/lib/quicsilver/server/request_handler.rb +111 -48
  75. data/lib/quicsilver/server/request_registry.rb +18 -5
  76. data/lib/quicsilver/server/scheduler.rb +50 -0
  77. data/lib/quicsilver/server/schedulers/fiber_scheduler.rb +79 -0
  78. data/lib/quicsilver/server/schedulers/thread_scheduler.rb +76 -0
  79. data/lib/quicsilver/server/server.rb +848 -104
  80. data/lib/quicsilver/server/web_transport_flow_control.rb +270 -0
  81. data/lib/quicsilver/server/web_transport_manager.rb +281 -0
  82. data/lib/quicsilver/server/web_transport_registry.rb +40 -0
  83. data/lib/quicsilver/server/web_transport_session.rb +812 -0
  84. data/lib/quicsilver/server/web_transport_stream.rb +326 -0
  85. data/lib/quicsilver/transport/configuration.rb +113 -11
  86. data/lib/quicsilver/transport/connection.rb +376 -132
  87. data/lib/quicsilver/transport/connection_stats.rb +53 -0
  88. data/lib/quicsilver/transport/event_loop.rb +36 -0
  89. data/lib/quicsilver/transport/inbound_stream.rb +21 -1
  90. data/lib/quicsilver/transport/stream.rb +18 -3
  91. data/lib/quicsilver/transport/stream_event.rb +25 -2
  92. data/lib/quicsilver/transport/stream_id.rb +26 -0
  93. data/lib/quicsilver/version.rb +1 -1
  94. data/lib/quicsilver.rb +45 -11
  95. data/lib/rackup/handler/quicsilver.rb +57 -38
  96. data/quicsilver.gemspec +17 -2
  97. metadata +120 -10
  98. data/benchmarks/concurrent.rb +0 -110
  99. data/benchmarks/quicsilver_server.rb +0 -46
  100. data/benchmarks/rails.rb +0 -170
  101. data/benchmarks/throughput.rb +0 -113
  102. data/examples/setup_certs.sh +0 -57
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Tests all major quicsilver features against a running Rails app.
4
+ #
5
+ # Prerequisites:
6
+ # 1. Start the Rails app: cd blogz && bundle exec rackup -s quicsilver -p 4433
7
+ # 2. Run this script: cd quicsilver && bundle exec ruby examples/rails_feature_test.rb
8
+ #
9
+ # Tests: ping, streaming, priorities, echo, HEAD, multiplexing, connection pooling
10
+
11
+ require "bundler/setup"
12
+ require "quicsilver"
13
+
14
+ HOST = "localhost"
15
+ PORT = 4433
16
+ PASS = "✅"
17
+ FAIL = "❌"
18
+
19
+ def test(name)
20
+ result = yield
21
+ puts " #{PASS} #{name}"
22
+ result
23
+ rescue => e
24
+ puts " #{FAIL} #{name}: #{e.message}"
25
+ nil
26
+ end
27
+
28
+ puts "Quicsilver Rails Feature Test"
29
+ puts " Target: https://#{HOST}:#{PORT}"
30
+ puts "=" * 60
31
+
32
+ # === 1. Basic connectivity ===
33
+ puts "\n1. Basic Connectivity"
34
+ puts "-" * 60
35
+
36
+ test("GET /h3/ping returns JSON over HTTP/3") do
37
+ response = Quicsilver::Client.get(HOST, PORT, "/h3/ping", unsecure: true)
38
+ raise "Expected 200, got #{response.status}" unless response.status == 200
39
+ raise "Expected JSON" unless response.body.include?('"status":"ok"')
40
+ raise "Expected HTTP/3" unless response.body.include?("HTTP/3")
41
+ puts " Response: #{response.body}"
42
+ end
43
+
44
+ # === 2. Connection Pooling ===
45
+ puts "\n2. Connection Pooling"
46
+ puts "-" * 60
47
+
48
+ test("5 requests reuse one connection (no repeat handshakes)") do
49
+ Quicsilver::Client.close_pool
50
+ times = 5.times.map do |i|
51
+ t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
52
+ Quicsilver::Client.get(HOST, PORT, "/h3/ping", unsecure: true)
53
+ elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
54
+ label = i == 0 ? "new connection + handshake" : "reused"
55
+ puts " Request #{i}: #{elapsed}ms (#{label})"
56
+ elapsed
57
+ end
58
+ puts " Pool: #{Quicsilver::Client.pool.size} connection(s)"
59
+ raise "Pool should have 1 connection" unless Quicsilver::Client.pool.size == 1
60
+ # First request includes handshake, subsequent should be faster on average
61
+ avg_reused = times[1..].sum / times[1..].size
62
+ puts " First request: #{times[0]}ms, avg reused: #{avg_reused.round(1)}ms"
63
+ end
64
+
65
+ # === 3. Streaming ===
66
+ puts "\n3. Streaming Response"
67
+ puts "-" * 60
68
+
69
+ test("GET /h3/stream returns chunked SSE data") do
70
+ response = Quicsilver::Client.get(HOST, PORT, "/h3/stream", unsecure: true)
71
+ raise "Expected 200" unless response.status == 200
72
+ chunks = response.body.scan(/data: chunk \d+/)
73
+ raise "Expected 5 chunks, got #{chunks.size}" unless chunks.size == 5
74
+ puts " Received #{chunks.size} streamed chunks"
75
+ end
76
+
77
+ # === 4. POST with Body (Echo) ===
78
+ puts "\n4. POST with Body"
79
+ puts "-" * 60
80
+
81
+ test("POST /h3/echo returns the request body") do
82
+ body = '{"message": "Hello HTTP/3!"}'
83
+ response = Quicsilver::Client.post(HOST, PORT, "/h3/echo",
84
+ body: body,
85
+ headers: { "content-type" => "application/json" },
86
+ unsecure: true)
87
+ raise "Expected 200" unless response.status == 200
88
+ raise "Body not echoed" unless response.body.include?("Hello HTTP/3!")
89
+ puts " Echoed: #{response.body[0..80]}"
90
+ end
91
+
92
+ # === 5. HEAD Request ===
93
+ puts "\n5. HEAD Request"
94
+ puts "-" * 60
95
+
96
+ test("HEAD /h3/head returns headers but no body") do
97
+ client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
98
+ response = client.head("/h3/head")
99
+ raise "Expected 200" unless response.status == 200
100
+ raise "HEAD should have empty body" unless response.body.nil? || response.body.empty?
101
+ puts " Status: #{response.status}, body size: #{response.body&.bytesize || 0}"
102
+ client.disconnect
103
+ end
104
+
105
+ # === 6. True Multiplexing (concurrent streams, single connection) ===
106
+ puts "\n6. True Multiplexing (concurrent streams, single connection)"
107
+ puts "-" * 60
108
+
109
+ test("10 concurrent requests on one connection") do
110
+ client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
111
+ client.get("/h3/ping") # warmup + handshake
112
+
113
+ t_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
114
+ requests = 10.times.map { client.build_request("GET", "/h3/ping") }
115
+ responses = requests.map { |r| r.response(timeout: 10) }
116
+ total = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t_start) * 1000).round(1)
117
+
118
+ ok = responses.count { |r| r.status == 200 }
119
+ rps = (10 / (total / 1000.0)).round(0)
120
+ puts " 10 concurrent requests in #{total}ms (#{ok}/10 ok, #{rps} req/s)"
121
+ raise "Not all 200" unless ok == 10
122
+ client.disconnect
123
+ end
124
+
125
+ # === 7. Priority Endpoints (CSS vs Image) ===
126
+ puts "\n7. Priority Endpoints"
127
+ puts "-" * 60
128
+
129
+ test("GET /h3/css returns CSS content") do
130
+ response = Quicsilver::Client.get(HOST, PORT, "/h3/css", unsecure: true)
131
+ raise "Expected 200" unless response.status == 200
132
+ puts " CSS: #{response.body.bytesize} bytes"
133
+ end
134
+
135
+ test("GET /h3/image returns large binary payload") do
136
+ response = Quicsilver::Client.get(HOST, PORT, "/h3/image", unsecure: true)
137
+ raise "Expected 200" unless response.status == 200
138
+ raise "Expected 50KB" unless response.body.bytesize == 50_000
139
+ puts " Image: #{response.body.bytesize} bytes"
140
+ end
141
+
142
+ # === 8. Slow Response (Delay) ===
143
+ puts "\n8. Slow Response"
144
+ puts "-" * 60
145
+
146
+ test("GET /h3/slow?delay=0.2 respects delay") do
147
+ t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
148
+ response = Quicsilver::Client.get(HOST, PORT, "/h3/slow?delay=0.2", unsecure: true)
149
+ elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
150
+ raise "Expected 200" unless response.status == 200
151
+ raise "Should take ~200ms, took #{elapsed}ms" unless elapsed > 150
152
+ puts " Delayed response in #{elapsed}ms"
153
+ end
154
+
155
+ # === 9. Concurrent Connections ===
156
+ puts "\n9. Concurrent Connections (4 threads, 4 connections)"
157
+ puts "-" * 60
158
+
159
+ test("4 concurrent requests on separate connections complete in parallel") do
160
+ Quicsilver::Client.close_pool
161
+ threads = 4.times.map do |i|
162
+ Thread.new do
163
+ client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
164
+ t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
165
+ response = client.get("/h3/slow?delay=0.2")
166
+ elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
167
+ client.disconnect
168
+ [i, response.status, elapsed]
169
+ end
170
+ end
171
+ results = threads.map(&:value).sort_by(&:first)
172
+ results.each { |i, status, elapsed| puts " Connection #{i}: #{status} -- #{elapsed}ms" }
173
+ total = results.map { |_, _, e| e }.max
174
+ sequential = 200 * 4
175
+ puts " Total: #{total}ms (sequential would be ~#{sequential}ms)"
176
+ end
177
+
178
+ # === 10. Large Upload ===
179
+ puts "\n10. Large Upload"
180
+ puts "-" * 60
181
+
182
+ test("POST 100KB body") do
183
+ big_body = "x" * 100_000
184
+ response = Quicsilver::Client.post(HOST, PORT, "/h3/upload",
185
+ body: big_body,
186
+ headers: { "content-type" => "application/octet-stream" },
187
+ unsecure: true)
188
+ raise "Expected 200" unless response.status == 200
189
+ raise "Size mismatch" unless response.body.include?('"received_bytes":100000')
190
+ puts " Uploaded 100KB, server received 100,000 bytes"
191
+ end
192
+
193
+ # === 11. Content-Length Validation ===
194
+ puts "\n11. Content-Length Validation"
195
+ puts "-" * 60
196
+
197
+ test("POST with content-length header") do
198
+ body = "hello"
199
+ response = Quicsilver::Client.post(HOST, PORT, "/h3/upload",
200
+ body: body,
201
+ headers: { "content-type" => "text/plain" },
202
+ unsecure: true)
203
+ raise "Expected 200, got #{response.status}" unless response.status == 200
204
+ raise "Size mismatch" unless response.body.include?('"received_bytes":5')
205
+ puts " Sent 5 bytes, server received 5 bytes"
206
+ end
207
+
208
+ # === 12. Handshake Cost (connection pooling benefit) ===
209
+ puts "\n12. Handshake Cost"
210
+ puts "-" * 60
211
+
212
+ test("Fresh connections pay handshake cost -- pooling avoids this") do
213
+ Quicsilver::Client.close_pool
214
+
215
+ # First connection -- full handshake
216
+ client1 = Quicsilver::Client.new(HOST, PORT, unsecure: true)
217
+ t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
218
+ client1.get("/h3/ping")
219
+ first = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t1) * 1000).round(1)
220
+ client1.disconnect
221
+
222
+ # Second connection -- another full handshake
223
+ client2 = Quicsilver::Client.new(HOST, PORT, unsecure: true)
224
+ t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
225
+ client2.get("/h3/ping")
226
+ second = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t2) * 1000).round(1)
227
+ client2.disconnect
228
+
229
+ # Pooled -- reuses existing connection, no handshake
230
+ Quicsilver::Client.close_pool
231
+ Quicsilver::Client.get(HOST, PORT, "/h3/ping", unsecure: true) # establish
232
+ t3 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
233
+ Quicsilver::Client.get(HOST, PORT, "/h3/ping", unsecure: true)
234
+ pooled = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t3) * 1000).round(1)
235
+
236
+ puts " Fresh connection 1: #{first}ms"
237
+ puts " Fresh connection 2: #{second}ms"
238
+ puts " Pooled (no handshake): #{pooled}ms"
239
+ end
240
+
241
+ # === 13. Server Health ===
242
+ puts "\n13. Server Health"
243
+ puts "-" * 60
244
+
245
+ test("Server still responding after all tests") do
246
+ response = Quicsilver::Client.get(HOST, PORT, "/h3/ping", unsecure: true)
247
+ raise "Expected 200" unless response.status == 200
248
+ puts " Server healthy"
249
+ end
250
+
251
+ # === 14. Rails CRUD ===
252
+ puts "\n14. Rails CRUD"
253
+ puts "-" * 60
254
+
255
+ test("GET /posts.json returns posts array") do
256
+ response = Quicsilver::Client.get(HOST, PORT, "/posts.json", unsecure: true)
257
+ raise "Expected 200" unless response.status == 200
258
+ puts " Posts: #{response.body[0..40]}"
259
+ end
260
+
261
+ # === 15. HTTP Methods ===
262
+ puts "\n15. HTTP Methods"
263
+ puts "-" * 60
264
+
265
+ client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
266
+ %i[get post put patch delete head].each do |method|
267
+ test("#{method.to_s.upcase} /h3/ping") do
268
+ response = client.public_send(method, "/h3/ping")
269
+ body_size = response.body&.bytesize || 0
270
+ puts " #{response.status} (#{body_size} bytes)"
271
+ end
272
+ end
273
+ client.disconnect
274
+
275
+ # === Summary ===
276
+ puts "\n" + "=" * 60
277
+ puts "Feature test complete!"
278
+ puts " Pool: #{Quicsilver::Client.pool.size} connection(s)"
279
+ Quicsilver::Client.close_pool
280
+ puts "Done"
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Demonstrates Quicsilver::Server#stats.
5
+ #
6
+ # ruby examples/server_stats.rb
7
+ # PORT=4444 ruby examples/server_stats.rb
8
+ #
9
+ # The stats snapshot combines:
10
+ # - Quicsilver app-server state: running, connections, active requests, queue.
11
+ # - QUIC transport counters: process-wide connection/stream/packet/worker state.
12
+
13
+ require_relative "example_helper"
14
+ require "json"
15
+
16
+ HOST = ENV.fetch("HOST", "localhost")
17
+ PORT = Integer(ENV.fetch("PORT", "4433"))
18
+
19
+ server = nil
20
+ server_thread = nil
21
+ clients = []
22
+
23
+ begin
24
+ app = ->(env) {
25
+ case env["PATH_INFO"]
26
+ when "/hello"
27
+ [200, { "content-type" => "text/plain" }, ["hello\n"]]
28
+ when %r{\A/slow}
29
+ sleep 0.25
30
+ [200, { "content-type" => "text/plain" }, ["slow response\n"]]
31
+ else
32
+ [404, { "content-type" => "text/plain" }, ["not found\n"]]
33
+ end
34
+ }
35
+
36
+ server = Quicsilver::Server.new(PORT, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
37
+ server_thread = Thread.new { server.start }
38
+
39
+ # Keep examples self-contained without requiring test helpers.
40
+ def wait_until(timeout: 3)
41
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
42
+ until yield
43
+ raise "timed out waiting" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
44
+
45
+ sleep 0.01
46
+ end
47
+ end
48
+
49
+ wait_until { server.running? }
50
+
51
+ # Print the useful subset first, then include the whole transport hash so it is
52
+ # easy to see every counter name currently exposed by Quicsilver.transport_counters.
53
+ def print_stats(title, server)
54
+ stats = server.stats
55
+ transport = stats["transport"] || {}
56
+
57
+ puts "\n#{title}"
58
+ puts "-" * title.length
59
+ puts JSON.pretty_generate(
60
+ "server" => {
61
+ "running" => stats["running"],
62
+ "shutting_down" => stats["shutting_down"],
63
+ "connections" => stats["connections"],
64
+ "requests" => stats["requests"],
65
+ "scheduler" => stats["scheduler"]
66
+ },
67
+ "transport_summary" => {
68
+ "connections_active" => transport["connections_active"],
69
+ "connections_connected" => transport["connections_connected"],
70
+ "streams_active" => transport["streams_active"],
71
+ "connection_queue_depth" => transport["connection_queue_depth"],
72
+ "connection_operations_queue_depth" => transport["connection_operations_queue_depth"],
73
+ "worker_operations_queue_depth" => transport["worker_operations_queue_depth"],
74
+ "connections_load_rejected" => transport["connections_load_rejected"],
75
+ "packets_dropped" => transport["packets_dropped"],
76
+ "app_bytes_received" => transport["app_bytes_received"],
77
+ "app_bytes_sent" => transport["app_bytes_sent"]
78
+ },
79
+ "transport" => transport
80
+ )
81
+ end
82
+
83
+ puts "Quicsilver Server Stats Demo"
84
+ puts "Listening on https://#{HOST}:#{PORT}"
85
+ puts "Note: transport counters are process-wide, so they include this example's clients too."
86
+
87
+ print_stats("1. Fresh server", server)
88
+
89
+ clients = 2.times.map { Quicsilver::Client.new(HOST, PORT, unsecure: true) }
90
+ clients.each_with_index do |client, index|
91
+ response = client.get("/hello")
92
+ puts "client #{index + 1}: GET /hello -> #{response.status}"
93
+ end
94
+
95
+ wait_until { server.stats.dig("connections", "active") >= 2 }
96
+ print_stats("2. After two client connections", server)
97
+
98
+ slow_requests = clients.each_with_index.map do |client, index|
99
+ Thread.new do
100
+ response = client.get("/slow/#{index + 1}")
101
+ puts "client #{index + 1}: GET /slow/#{index + 1} -> #{response.status}"
102
+ end
103
+ end
104
+
105
+ wait_until { server.stats.dig("requests", "active") >= 2 }
106
+ print_stats("3. While slow requests are in flight", server)
107
+
108
+ slow_requests.each(&:join)
109
+ print_stats("4. After slow requests complete", server)
110
+
111
+ puts "\nDone."
112
+ ensure
113
+ clients.each { |client| client.disconnect rescue nil }
114
+ Quicsilver::Client.close_pool rescue nil
115
+ server&.stop rescue nil
116
+ server_thread&.join(2)
117
+ end
@@ -7,15 +7,15 @@ puts "🔌 Simple HTTP/3 Client Test"
7
7
  puts "=" * 40
8
8
 
9
9
  begin
10
- client = Quicsilver::Client.new("127.0.0.1", 4433, unsecure: true)
10
+ client = Quicsilver::Client.new("localhost", 4433, unsecure: true)
11
11
 
12
12
  client.connect
13
13
 
14
- response = client.get("/posts")
14
+ response = client.get("/")
15
15
 
16
- puts "Status: #{response[:status]}"
17
- puts "Headers: #{response[:headers].inspect}"
18
- puts "Body: #{response[:body]}"
16
+ puts "Status: #{response.status}"
17
+ puts "Headers: #{response.headers.inspect}"
18
+ puts "Body: #{response.body}"
19
19
 
20
20
  rescue => e
21
21
  puts "❌ Error: #{e.class} - #{e.message}"
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Server-Sent Events over HTTP/3.
4
+ #
5
+ # Demonstrates streaming responses — data arrives chunk by chunk,
6
+ # not buffered. Each chunk is sent as a separate HTTP/3 DATA frame.
7
+ #
8
+ # ruby examples/streaming_sse.rb
9
+ # curl --http3-only -k https://localhost:4433/
10
+
11
+ require_relative "example_helper"
12
+
13
+ app = ->(env) {
14
+ body = Enumerator.new do |y|
15
+ 10.times do |i|
16
+ y << "data: {\"count\":#{i},\"time\":\"#{Time.now.iso8601}\"}\n\n"
17
+ sleep 0.2
18
+ end
19
+ y << "data: {\"done\":true}\n\n"
20
+ end
21
+
22
+ [200, { "content-type" => "text/event-stream", "cache-control" => "no-cache" }, body]
23
+ }
24
+
25
+ server = Quicsilver::Server.new(4433, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
26
+
27
+ puts "📡 Streaming SSE over HTTP/3"
28
+ puts " https://localhost:4433"
29
+ puts " curl --http3-only -k https://localhost:4433/"
30
+ puts " Events arrive every 200ms — no buffering."
31
+ puts
32
+
33
+ server.start
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # HTTP/3 Trailers — send headers after the body.
4
+ #
5
+ # Trailers let the server report status after streaming is complete.
6
+ # Useful for checksums, streaming error status, and gRPC.
7
+ #
8
+ # ruby examples/trailers.rb
9
+
10
+ require_relative "example_helper"
11
+ require "digest"
12
+
13
+ PORT = 4433
14
+ HOST = "localhost"
15
+
16
+ app = ->(env) {
17
+ path = env["PATH_INFO"]
18
+
19
+ case path
20
+ when "/stream-with-checksum"
21
+ body = ["chunk1\n", "chunk2\n", "chunk3\n"]
22
+ checksum = Digest::SHA256.hexdigest(body.join)[0..7]
23
+ env["rack.trailers"] = { "x-checksum" => checksum }
24
+ [200, { "content-type" => "text/plain" }, body]
25
+
26
+ when "/grpc-style"
27
+ env["rack.trailers"] = { "grpc-status" => "0", "grpc-message" => "OK" }
28
+ [200, { "content-type" => "application/grpc" }, ['{"result":"processed"}']]
29
+
30
+ when "/grpc-error"
31
+ env["rack.trailers"] = { "grpc-status" => "13", "grpc-message" => "INTERNAL" }
32
+ [200, { "content-type" => "application/grpc" }, ['{"error":"something broke"}']]
33
+
34
+ else
35
+ [200, { "content-type" => "text/plain" }, ["Try /stream-with-checksum, /grpc-style, or /grpc-error"]]
36
+ end
37
+ }
38
+
39
+ server = Quicsilver::Server.new(PORT, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
40
+ server_thread = Thread.new { server.start }
41
+ sleep 0.3
42
+
43
+ puts "HTTP/3 Trailers Demo"
44
+ puts "=" * 50
45
+
46
+ client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
47
+
48
+ puts "\n Streaming with checksum trailer:"
49
+ response = client.get("/stream-with-checksum")
50
+ puts " Status: #{response.status}"
51
+ puts " Body: #{response.body.inspect}"
52
+ puts " Trailers: #{response.trailers}"
53
+
54
+ puts "\n gRPC-style success:"
55
+ response = client.get("/grpc-style")
56
+ puts " Status: #{response.status}"
57
+ puts " Body: #{response.body}"
58
+ puts " Trailers: #{response.trailers}"
59
+
60
+ puts "\n gRPC-style error:"
61
+ response = client.get("/grpc-error")
62
+ puts " Status: #{response.status}"
63
+ puts " Body: #{response.body}"
64
+ puts " Trailers: #{response.trailers}"
65
+
66
+ client.disconnect
67
+ server.stop
68
+ server_thread.join(2)
69
+ puts "\nDone"