quicsilver 0.2.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 +4 -5
- data/.github/workflows/cibuildgem.yaml +93 -0
- data/.gitignore +3 -1
- data/CHANGELOG.md +81 -0
- data/Gemfile.lock +26 -4
- data/README.md +95 -31
- data/Rakefile +95 -3
- data/benchmarks/components.rb +191 -0
- data/benchmarks/concurrent.rb +110 -0
- data/benchmarks/helpers.rb +88 -0
- data/benchmarks/quicsilver_server.rb +1 -1
- data/benchmarks/rails.rb +170 -0
- data/benchmarks/throughput.rb +113 -0
- 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 +568 -181
- data/lib/quicsilver/client/client.rb +349 -0
- data/lib/quicsilver/client/connection_pool.rb +106 -0
- data/lib/quicsilver/client/request.rb +98 -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/{http3.rb → protocol/frames.rb} +146 -30
- data/lib/quicsilver/protocol/priority.rb +56 -0
- data/lib/quicsilver/protocol/qpack/decoder.rb +165 -0
- data/lib/quicsilver/protocol/qpack/encoder.rb +227 -0
- data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +140 -0
- data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
- data/lib/quicsilver/protocol/request_encoder.rb +47 -0
- data/lib/quicsilver/protocol/request_parser.rb +275 -0
- data/lib/quicsilver/protocol/response_encoder.rb +97 -0
- data/lib/quicsilver/protocol/response_parser.rb +141 -0
- 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/listener_data.rb +14 -0
- data/lib/quicsilver/server/request_handler.rb +138 -0
- data/lib/quicsilver/server/request_registry.rb +50 -0
- data/lib/quicsilver/server/server.rb +610 -0
- data/lib/quicsilver/transport/configuration.rb +141 -0
- data/lib/quicsilver/transport/connection.rb +379 -0
- data/lib/quicsilver/transport/event_loop.rb +38 -0
- data/lib/quicsilver/transport/inbound_stream.rb +33 -0
- data/lib/quicsilver/transport/stream.rb +28 -0
- data/lib/quicsilver/transport/stream_event.rb +26 -0
- data/lib/quicsilver/version.rb +1 -1
- data/lib/quicsilver.rb +55 -14
- data/lib/rackup/handler/quicsilver.rb +1 -2
- data/quicsilver.gemspec +13 -3
- metadata +125 -21
- data/benchmarks/benchmark.rb +0 -68
- data/examples/setup_certs.sh +0 -57
- data/lib/quicsilver/client.rb +0 -261
- data/lib/quicsilver/connection.rb +0 -42
- data/lib/quicsilver/event_loop.rb +0 -38
- data/lib/quicsilver/http3/request_encoder.rb +0 -133
- data/lib/quicsilver/http3/request_parser.rb +0 -176
- data/lib/quicsilver/http3/response_encoder.rb +0 -186
- data/lib/quicsilver/http3/response_parser.rb +0 -160
- data/lib/quicsilver/listener_data.rb +0 -29
- data/lib/quicsilver/quic_stream.rb +0 -36
- data/lib/quicsilver/request_registry.rb +0 -48
- data/lib/quicsilver/server.rb +0 -355
- data/lib/quicsilver/server_configuration.rb +0 -78
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d4926d24fbce141791505bb14d59e543a30a6c5cbf8ca3c3fd36c9123b2941db
|
|
4
|
+
data.tar.gz: 45c128c6bf9fc48b8267ba2927414f8429607e8f99e4169235d12ebe26863ccc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ad6f92657375909bac67ce88bfa64d47c53a2dd69bb424a36f297927778389247714c092b004287cc278c5ecdf94c0f186a4c15f4fa4cfff62dbef9fcfc155f6
|
|
7
|
+
data.tar.gz: 1f5d2812bfb18a15b1821c8afcb18f779663ec3066495ef69dfd9a1c79f8d83f13f7c6287b8ce56be318476ea69fd6c520c1dc4298ceb807c64f6de0747b3035
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -22,15 +22,14 @@ jobs:
|
|
|
22
22
|
runs-on: ubuntu-24.04
|
|
23
23
|
strategy:
|
|
24
24
|
matrix:
|
|
25
|
-
ruby-version: ["3.
|
|
25
|
+
ruby-version: ["3.4"]
|
|
26
26
|
|
|
27
27
|
steps:
|
|
28
28
|
- uses: actions/checkout@v4
|
|
29
|
+
with:
|
|
30
|
+
submodules: recursive
|
|
29
31
|
- name: Set up Ruby
|
|
30
|
-
|
|
31
|
-
# change this to (see https://github.com/ruby/setup-ruby#versioning):
|
|
32
|
-
# uses: ruby/setup-ruby@v1
|
|
33
|
-
uses: ruby/setup-ruby@55283cc23133118229fd3f97f9336ee23a179fcf # v1.146.0
|
|
32
|
+
uses: ruby/setup-ruby@v1
|
|
34
33
|
with:
|
|
35
34
|
ruby-version: ${{ matrix.ruby-version }}
|
|
36
35
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
name: "Package and release gems with precompiled binaries"
|
|
2
|
+
on:
|
|
3
|
+
workflow_dispatch:
|
|
4
|
+
inputs:
|
|
5
|
+
release:
|
|
6
|
+
description: "If the whole build passes on all platforms, release the gems on RubyGems.org"
|
|
7
|
+
required: false
|
|
8
|
+
type: boolean
|
|
9
|
+
default: false
|
|
10
|
+
env:
|
|
11
|
+
CIBUILDGEM: 1
|
|
12
|
+
jobs:
|
|
13
|
+
compile:
|
|
14
|
+
timeout-minutes: 20
|
|
15
|
+
name: "Cross compile the gem on different ruby versions"
|
|
16
|
+
strategy:
|
|
17
|
+
matrix:
|
|
18
|
+
os: ["macos-latest", "ubuntu-22.04"]
|
|
19
|
+
runs-on: "${{ matrix.os }}"
|
|
20
|
+
steps:
|
|
21
|
+
- name: "Checkout code"
|
|
22
|
+
uses: "actions/checkout@v5"
|
|
23
|
+
with:
|
|
24
|
+
submodules: recursive
|
|
25
|
+
- name: "Setup Ruby"
|
|
26
|
+
uses: "ruby/setup-ruby@v1"
|
|
27
|
+
with:
|
|
28
|
+
ruby-version: "3.4"
|
|
29
|
+
bundler-cache: true
|
|
30
|
+
- name: "Build MsQuic"
|
|
31
|
+
run: bundle exec rake build_msquic
|
|
32
|
+
- name: "Run cibuildgem"
|
|
33
|
+
uses: "shopify/cibuildgem/.github/actions/cibuildgem@main"
|
|
34
|
+
with:
|
|
35
|
+
step: "compile"
|
|
36
|
+
test:
|
|
37
|
+
timeout-minutes: 20
|
|
38
|
+
name: "Run the test suite"
|
|
39
|
+
needs: compile
|
|
40
|
+
strategy:
|
|
41
|
+
matrix:
|
|
42
|
+
os: ["macos-latest", "ubuntu-22.04"]
|
|
43
|
+
rubies: ["3.4", "4.0"]
|
|
44
|
+
type: ["cross", "native"]
|
|
45
|
+
runs-on: "${{ matrix.os }}"
|
|
46
|
+
steps:
|
|
47
|
+
- name: "Checkout code"
|
|
48
|
+
uses: "actions/checkout@v5"
|
|
49
|
+
- name: "Setup Ruby"
|
|
50
|
+
uses: "ruby/setup-ruby@v1"
|
|
51
|
+
with:
|
|
52
|
+
ruby-version: "${{ matrix.rubies }}"
|
|
53
|
+
bundler-cache: true
|
|
54
|
+
- name: "Run cibuildgem"
|
|
55
|
+
uses: "shopify/cibuildgem/.github/actions/cibuildgem@main"
|
|
56
|
+
with:
|
|
57
|
+
step: "test_${{ matrix.type }}"
|
|
58
|
+
install:
|
|
59
|
+
timeout-minutes: 5
|
|
60
|
+
name: "Verify the gem can be installed"
|
|
61
|
+
needs: test
|
|
62
|
+
strategy:
|
|
63
|
+
matrix:
|
|
64
|
+
os: ["macos-latest", "ubuntu-22.04"]
|
|
65
|
+
runs-on: "${{ matrix.os }}"
|
|
66
|
+
steps:
|
|
67
|
+
- name: "Setup Ruby"
|
|
68
|
+
uses: "ruby/setup-ruby@v1"
|
|
69
|
+
with:
|
|
70
|
+
ruby-version: "4.0.0"
|
|
71
|
+
- name: "Run cibuildgem"
|
|
72
|
+
uses: "shopify/cibuildgem/.github/actions/cibuildgem@main"
|
|
73
|
+
with:
|
|
74
|
+
step: "install"
|
|
75
|
+
release:
|
|
76
|
+
environment: release
|
|
77
|
+
permissions:
|
|
78
|
+
id-token: write
|
|
79
|
+
contents: read
|
|
80
|
+
timeout-minutes: 5
|
|
81
|
+
if: ${{ inputs.release }}
|
|
82
|
+
name: "Release all gems with RubyGems"
|
|
83
|
+
needs: install
|
|
84
|
+
runs-on: "ubuntu-latest"
|
|
85
|
+
steps:
|
|
86
|
+
- name: "Setup Ruby"
|
|
87
|
+
uses: "ruby/setup-ruby@v1"
|
|
88
|
+
with:
|
|
89
|
+
ruby-version: "4.0.0"
|
|
90
|
+
- name: "Run cibuildgem"
|
|
91
|
+
uses: "shopify/cibuildgem/.github/actions/cibuildgem@main"
|
|
92
|
+
with:
|
|
93
|
+
step: "release"
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,87 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.0] - 2026-04-25
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Client connection pool with automatic reuse (`Quicsilver::Client.get/post` class-level API)
|
|
12
|
+
- GREASE support (RFC 9297) — settings, frames, and unidirectional streams
|
|
13
|
+
- GOAWAY validation (RFC 9114 §7.2.6) — monotonically decreasing IDs, stream ID validation
|
|
14
|
+
- Trailer support (RFC 9114 §4.1) — parse and send trailing HEADERS frames
|
|
15
|
+
- Extensible Priorities (RFC 9218) — parse `priority` header, PRIORITY_UPDATE frames on control stream, MsQuic stream priority mapping
|
|
16
|
+
- FrameParser base class — unified frame walking, ordering, body accumulation, size limits
|
|
17
|
+
- FrameReader module — shared byte-level frame extraction for request/response/control streams
|
|
18
|
+
- Trailer wiring in Adapter and StreamOutput for protocol-http integration
|
|
19
|
+
- Informational 1xx responses (§4.1) — 103 Early Hints with `rack.early_hints` support for Rails
|
|
20
|
+
- Two-phase GOAWAY shutdown (§5.2) — server sends decreasing GOAWAY IDs during graceful shutdown
|
|
21
|
+
- Client processes server SETTINGS (§7.2.4) — parses peer's SETTINGS including MAX_FIELD_SECTION_SIZE
|
|
22
|
+
- Client processes server GOAWAY (§5.2) — tracks peer_goaway_id, blocks new requests, connection pool evicts draining connections
|
|
23
|
+
- MIT license in gemspec
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- QPACK prefix decoding — decode Required Insert Count and Delta Base as varints instead of hardcoded `offset = 2`
|
|
27
|
+
- Default decoder rejects payloads referencing the dynamic table
|
|
28
|
+
- Response parser now enforces `max_frame_payload_size` (was missing)
|
|
29
|
+
- Duplicate `frames` method in FrameParser
|
|
30
|
+
- Consistent `@headers` and `@trailers` initialization (`{}` not `nil`)
|
|
31
|
+
- extconf.rb — force Apple clang on macOS (Homebrew clang produces broken MsQuic binaries)
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- RequestParser and ResponseParser inherit from FrameParser (reduced ~230 lines of duplication)
|
|
35
|
+
- `store_header`, `body`, `DEFAULT_DECODER`, `EMPTY_BODY`, `parse!` moved to FrameParser base class
|
|
36
|
+
- `@body_io` renamed to `@body` in ResponseParser for consistency
|
|
37
|
+
- ResponseEncoder accepts optional `trailers:` hash
|
|
38
|
+
- StreamOutput accepts `send_fin:` parameter for trailer support
|
|
39
|
+
|
|
40
|
+
## [0.3.0] - 2026-03-23
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
- QPACK Huffman coding with 8-bit decode table and encode/decode caching
|
|
44
|
+
- 0-RTT replay protection for unsafe HTTP methods
|
|
45
|
+
- Bounded backpressure support
|
|
46
|
+
- Buffer size limits to prevent memory exhaustion (configurable `max_body_size`, `max_header_size`, `max_header_count`, `max_frame_payload_size`)
|
|
47
|
+
- Content-length validation
|
|
48
|
+
- Multi-value header support for duplicate header fields
|
|
49
|
+
- Headers validation: reject connection-specific headers, require `:authority` or `host` for http/https schemes
|
|
50
|
+
- Incremental unidirectional stream processing with critical stream protection
|
|
51
|
+
- QPACK encoder and decoder stream instruction validation
|
|
52
|
+
- Spec-correct error signaling with error codes on `FrameError` and `MessageError`
|
|
53
|
+
- Suppress response body for HEAD requests per RFC 9114 §4.1
|
|
54
|
+
- Allow `te: trailers` header in requests per RFC 9114 §4.2
|
|
55
|
+
- Custom ALPN support (no longer hardcoded to `h3`)
|
|
56
|
+
- `Stream` and `StreamEvent` abstractions to encapsulate C extension details
|
|
57
|
+
- Dual-stack (IPv4/IPv6) listener support — fixes TLS handshake failures on macOS
|
|
58
|
+
- Client `PUT` method
|
|
59
|
+
- Integration test suite for curl HTTP/3
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
- Memory leaks: free `StreamContext` on `SHUTDOWN_COMPLETE`, free `ConnectionContext` on `CONNECTION_SHUTDOWN_COMPLETE`, close `EventQ`/`ExecContext`/`WakeFd` on shutdown
|
|
63
|
+
- Double-free and handle leaks in C extension
|
|
64
|
+
- `dispatch_to_ruby` safety with `rb_protect`; client use-after-free fix
|
|
65
|
+
- Infinite loop on truncated varint in request/response parsers
|
|
66
|
+
- Frame ordering: `DATA` before `HEADERS` now raises `FrameError`
|
|
67
|
+
- `STOP_SENDING` / `STREAM_RESET` compliance — server properly cancels streams and resets send side
|
|
68
|
+
- Control stream validation: reject duplicate settings, forbidden frame types, and reserved HTTP/2 types
|
|
69
|
+
- QPACK static table index 57/58 casing (`includeSubDomains`)
|
|
70
|
+
- Stale stream handle guard in cancel and C extension
|
|
71
|
+
- Replaced `Thread.kill` with `Thread.raise(DrainTimeoutError)` for clean drain
|
|
72
|
+
- Binary encoding for `buffer_data` and empty FIN handling
|
|
73
|
+
- Linux/GitHub CI: use epoll instead of kqueue on non-Darwin platforms
|
|
74
|
+
- Circular require warning
|
|
75
|
+
|
|
76
|
+
### Changed
|
|
77
|
+
- Reorganized gem structure: `protocol/`, `server/`, `transport/` directories
|
|
78
|
+
- Server owns the 0-RTT policy
|
|
79
|
+
- QPACK encoder uses O(1) static table lookup with multi-level caching
|
|
80
|
+
- QPACK decoder uses string-based decoding with result caching
|
|
81
|
+
- HTTP/3 parsers optimized with parse-level caching and lazy allocation
|
|
82
|
+
- Varint encoding/decoding optimized with precomputed tables
|
|
83
|
+
- HTTP/3 encoders handle framing only; QPACK handles field encoding (cleaner separation)
|
|
84
|
+
- MsQuic custom execution mode with configurable worker pool and throughput settings
|
|
85
|
+
|
|
86
|
+
### Limitations
|
|
87
|
+
- Client does not reuse connections
|
|
88
|
+
|
|
8
89
|
## [0.2.0] - 2025-12-17
|
|
9
90
|
|
|
10
91
|
### Added
|
data/Gemfile.lock
CHANGED
|
@@ -1,22 +1,43 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
quicsilver (0.
|
|
4
|
+
quicsilver (0.4.0)
|
|
5
|
+
console
|
|
5
6
|
localhost (~> 1.6)
|
|
7
|
+
logger
|
|
8
|
+
protocol-http (~> 0.49)
|
|
9
|
+
protocol-rack (~> 0.22)
|
|
6
10
|
rack (~> 3.0)
|
|
7
11
|
rackup (~> 2.0)
|
|
8
12
|
|
|
9
13
|
GEM
|
|
10
14
|
remote: https://rubygems.org/
|
|
11
15
|
specs:
|
|
16
|
+
benchmark-ips (2.14.0)
|
|
17
|
+
console (1.34.3)
|
|
18
|
+
fiber-annotation
|
|
19
|
+
fiber-local (~> 1.1)
|
|
20
|
+
json
|
|
21
|
+
fiber-annotation (0.2.0)
|
|
22
|
+
fiber-local (1.1.0)
|
|
23
|
+
fiber-storage
|
|
24
|
+
fiber-storage (1.0.1)
|
|
25
|
+
io-stream (0.11.1)
|
|
26
|
+
json (2.15.2)
|
|
12
27
|
localhost (1.6.0)
|
|
13
|
-
|
|
28
|
+
logger (1.7.0)
|
|
29
|
+
minitest (5.27.0)
|
|
14
30
|
minitest-focus (1.4.0)
|
|
15
31
|
minitest (>= 4, < 6)
|
|
32
|
+
protocol-http (0.60.0)
|
|
33
|
+
protocol-rack (0.22.1)
|
|
34
|
+
io-stream (>= 0.10)
|
|
35
|
+
protocol-http (~> 0.58)
|
|
36
|
+
rack (>= 1.0)
|
|
16
37
|
rack (3.2.4)
|
|
17
38
|
rackup (2.2.1)
|
|
18
39
|
rack (>= 3)
|
|
19
|
-
rake (
|
|
40
|
+
rake (13.4.2)
|
|
20
41
|
rake-compiler (1.3.0)
|
|
21
42
|
rake
|
|
22
43
|
rake-compiler-dock (1.9.1)
|
|
@@ -26,11 +47,12 @@ PLATFORMS
|
|
|
26
47
|
ruby
|
|
27
48
|
|
|
28
49
|
DEPENDENCIES
|
|
50
|
+
benchmark-ips (~> 2.12)
|
|
29
51
|
bundler (~> 2.0)
|
|
30
52
|
minitest (~> 5.0)
|
|
31
53
|
minitest-focus (~> 1.3)
|
|
32
54
|
quicsilver!
|
|
33
|
-
rake (~>
|
|
55
|
+
rake (~> 13.0)
|
|
34
56
|
rake-compiler (~> 1.2)
|
|
35
57
|
rake-compiler-dock (~> 1.3)
|
|
36
58
|
|
data/README.md
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
# Quicsilver
|
|
2
2
|
|
|
3
|
-
HTTP/3 server for Ruby with Rack support.
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
HTTP/3 server and client for Ruby with Rack support.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **HTTP/3 server** — serve any Rack app over QUIC/HTTP/3
|
|
8
|
+
- **HTTP/3 client** — make requests with automatic connection pooling
|
|
9
|
+
- **Rack integration** — `rackup -s quicsilver` works with Rails, Sinatra, any Rack app
|
|
10
|
+
- **Streaming** — dispatch on HEADERS, stream body chunks as they arrive
|
|
11
|
+
- **Extensible Priorities** (RFC 9218) — CSS before images, server respects client priority hints
|
|
12
|
+
- **Trailers** (RFC 9114 §4.1) — send/receive trailing headers after the body
|
|
13
|
+
- **GREASE** (RFC 9297) — extensibility testing on settings, frames, and streams
|
|
14
|
+
- **GOAWAY** (RFC 9114 §7.2.6) — graceful connection draining with validation
|
|
15
|
+
- **0-RTT** — fast reconnection with replay protection
|
|
16
|
+
- **Connection pooling** — client reuses connections automatically
|
|
17
|
+
- **protocol-http integration** — works with Falcon and protocol-http ecosystem
|
|
6
18
|
|
|
7
19
|
## Installation
|
|
8
20
|
|
|
9
21
|
```bash
|
|
10
|
-
git clone
|
|
22
|
+
git clone https://github.com/hahmed/quicsilver
|
|
11
23
|
cd quicsilver
|
|
12
24
|
bundle install
|
|
13
25
|
rake compile
|
|
@@ -21,18 +33,11 @@ rake compile
|
|
|
21
33
|
require "quicsilver"
|
|
22
34
|
|
|
23
35
|
app = ->(env) {
|
|
24
|
-
|
|
25
|
-
when '/'
|
|
26
|
-
[200, {'content-type' => 'text/plain'}, ["Hello HTTP/3!"]]
|
|
27
|
-
when '/api/users'
|
|
28
|
-
[200, {'content-type' => 'application/json'}, ['{"users": ["alice", "bob"]}']]
|
|
29
|
-
else
|
|
30
|
-
[404, {'content-type' => 'text/plain'}, ["Not Found"]]
|
|
31
|
-
end
|
|
36
|
+
[200, {"content-type" => "text/plain"}, ["Hello HTTP/3!"]]
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
server = Quicsilver::Server.new(4433, app: app)
|
|
35
|
-
server.start
|
|
40
|
+
server.start
|
|
36
41
|
```
|
|
37
42
|
|
|
38
43
|
### Client
|
|
@@ -40,43 +45,102 @@ server.start # Blocks until shutdown
|
|
|
40
45
|
```ruby
|
|
41
46
|
require "quicsilver"
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
response
|
|
47
|
-
puts response[:body]
|
|
48
|
+
# Class-level API with automatic connection pooling
|
|
49
|
+
response = Quicsilver::Client.get("127.0.0.1", 4433, "/")
|
|
50
|
+
puts response[:status] # => 200
|
|
51
|
+
puts response[:body] # => "Hello HTTP/3!"
|
|
48
52
|
|
|
49
|
-
|
|
53
|
+
# POST with body
|
|
54
|
+
response = Quicsilver::Client.post("127.0.0.1", 4433, "/api/users",
|
|
55
|
+
body: '{"name": "alice"}',
|
|
56
|
+
headers: { "content-type" => "application/json" })
|
|
50
57
|
|
|
58
|
+
# Instance-level for more control
|
|
59
|
+
client = Quicsilver::Client.new("127.0.0.1", 4433, unsecure: true)
|
|
60
|
+
response = client.get("/")
|
|
51
61
|
client.disconnect
|
|
52
62
|
```
|
|
53
63
|
|
|
54
|
-
|
|
64
|
+
### Rails
|
|
55
65
|
|
|
56
66
|
```bash
|
|
57
67
|
rackup -s quicsilver -p 4433
|
|
58
68
|
```
|
|
59
69
|
|
|
70
|
+
### curl
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
curl --http3-only https://localhost:4433/
|
|
74
|
+
```
|
|
75
|
+
|
|
60
76
|
## Configuration
|
|
61
77
|
|
|
62
78
|
```ruby
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
79
|
+
config = Quicsilver::Transport::Configuration.new(
|
|
80
|
+
"certificates/server.crt",
|
|
81
|
+
"certificates/server.key",
|
|
82
|
+
idle_timeout_ms: 10_000,
|
|
83
|
+
max_concurrent_requests: 100,
|
|
84
|
+
max_body_size: 10 * 1024 * 1024, # 10MB body limit (optional)
|
|
85
|
+
max_header_size: 64 * 1024, # 64KB header limit (optional)
|
|
86
|
+
max_header_count: 128, # Header count limit (optional)
|
|
87
|
+
stream_receive_window: 262_144, # 256KB per stream
|
|
88
|
+
connection_flow_control_window: 16_777_216 # 16MB per connection
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
server = Quicsilver::Server.new(4433, app: app, server_configuration: config)
|
|
92
|
+
server.start
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Priorities
|
|
96
|
+
|
|
97
|
+
Browsers send priority hints on requests. Quicsilver parses them and tells MsQuic to schedule high-priority streams first.
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
GET /style.css → priority: u=0 → sent first (highest urgency)
|
|
101
|
+
GET /app.js → priority: u=1 → sent second
|
|
102
|
+
GET /hero.png → priority: u=5 → sent later
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
No configuration needed — it works automatically.
|
|
106
|
+
|
|
107
|
+
## Trailers
|
|
108
|
+
|
|
109
|
+
Send headers after the body — useful for checksums, streaming status, and gRPC.
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
# Trailers work with protocol-http's Headers#trailer! API
|
|
113
|
+
headers = Protocol::HTTP::Headers.new
|
|
114
|
+
headers.add("content-type", "text/plain")
|
|
115
|
+
headers.trailer!
|
|
116
|
+
headers.add("x-checksum", "abc123")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Protocol-HTTP Mode
|
|
120
|
+
|
|
121
|
+
For integration with [Falcon](https://github.com/socketry/falcon) and the protocol-http ecosystem:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
config = Quicsilver::Transport::Configuration.new(
|
|
125
|
+
"certificates/server.crt",
|
|
126
|
+
"certificates/server.key",
|
|
127
|
+
mode: :protocol_http
|
|
71
128
|
)
|
|
129
|
+
|
|
130
|
+
server = Quicsilver::Server.new(4433, app: app, server_configuration: config)
|
|
131
|
+
server.start
|
|
72
132
|
```
|
|
73
133
|
|
|
134
|
+
| Mode | Body Handling | Use Case |
|
|
135
|
+
|------|---------------|----------|
|
|
136
|
+
| `:rack` (default) | Buffered | Standard Rack apps |
|
|
137
|
+
| `:protocol_http` | Streaming | Falcon, protocol-http apps |
|
|
138
|
+
|
|
74
139
|
## Development
|
|
75
140
|
|
|
76
141
|
```bash
|
|
77
|
-
rake compile # Build C extension
|
|
78
|
-
rake test # Run tests
|
|
79
|
-
rake clean # Clean build artifacts
|
|
142
|
+
rake compile # Build C extension (macOS: uses Apple clang automatically)
|
|
143
|
+
rake test # Run tests
|
|
80
144
|
```
|
|
81
145
|
|
|
82
146
|
## License
|
data/Rakefile
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "bundler/setup"
|
|
1
2
|
require "bundler/gem_tasks"
|
|
2
3
|
require "rake/testtask"
|
|
3
4
|
require "rake/extensiontask"
|
|
@@ -6,6 +7,27 @@ Rake::ExtensionTask.new('quicsilver') do |ext|
|
|
|
6
7
|
ext.lib_dir = 'lib/quicsilver'
|
|
7
8
|
end
|
|
8
9
|
|
|
10
|
+
# Ensure MsQuic is built before compiling the C extension
|
|
11
|
+
task :compile => :build_msquic
|
|
12
|
+
|
|
13
|
+
# Copy MsQuic dylib next to the compiled extension for gem packaging
|
|
14
|
+
task :bundle_msquic do
|
|
15
|
+
lib_dir = 'lib/quicsilver'
|
|
16
|
+
if RUBY_PLATFORM =~ /darwin/
|
|
17
|
+
dylib = 'vendor/msquic/build/bin/Release/libmsquic.2.dylib'
|
|
18
|
+
if File.exist?(dylib)
|
|
19
|
+
cp dylib, "#{lib_dir}/libmsquic.2.dylib"
|
|
20
|
+
# Update rpath so the bundle finds the dylib in the same directory
|
|
21
|
+
sh "install_name_tool -add_rpath @loader_path #{lib_dir}/quicsilver.bundle 2>/dev/null || true"
|
|
22
|
+
end
|
|
23
|
+
elsif RUBY_PLATFORM =~ /linux/
|
|
24
|
+
so = 'vendor/msquic/build/bin/Release/libmsquic.so.2'
|
|
25
|
+
cp so, "#{lib_dir}/libmsquic.so.2" if File.exist?(so)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
task :build => :bundle_msquic
|
|
30
|
+
|
|
9
31
|
task :setup do
|
|
10
32
|
# Initialize git submodule if it doesn't exist
|
|
11
33
|
unless File.exist?('vendor/msquic')
|
|
@@ -15,9 +37,18 @@ task :setup do
|
|
|
15
37
|
end
|
|
16
38
|
|
|
17
39
|
task :build_msquic => :setup do
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
40
|
+
cmake_args = ['-B build', '-DCMAKE_BUILD_TYPE=Release']
|
|
41
|
+
if RUBY_PLATFORM =~ /darwin/
|
|
42
|
+
cmake_args << '-DCMAKE_EXE_LINKER_FLAGS="-framework CoreServices"'
|
|
43
|
+
cmake_args << '-DCMAKE_SHARED_LINKER_FLAGS="-framework CoreServices"'
|
|
44
|
+
# Ensure QuicTLS uses Xcode SDK, not Homebrew OpenSSL
|
|
45
|
+
sdk_path = `xcrun --show-sdk-path 2>/dev/null`.strip
|
|
46
|
+
cmake_args << "-DCMAKE_OSX_SYSROOT=#{sdk_path}" unless sdk_path.empty?
|
|
47
|
+
end
|
|
48
|
+
# Override PATH so QuicTLS openssldir detection finds system openssl, not Homebrew
|
|
49
|
+
env = { 'PATH' => "/usr/bin:#{ENV['PATH']}" }
|
|
50
|
+
sh env, "cd vendor/msquic && cmake #{cmake_args.join(' ')}"
|
|
51
|
+
sh env, 'cd vendor/msquic && cmake --build build --config Release'
|
|
21
52
|
end
|
|
22
53
|
|
|
23
54
|
task :build => [:build_msquic, :compile]
|
|
@@ -28,4 +59,65 @@ Rake::TestTask.new(:test) do |t|
|
|
|
28
59
|
t.test_files = FileList["test/**/*_test.rb"]
|
|
29
60
|
end
|
|
30
61
|
|
|
62
|
+
Rake::TestTask.new(:test_unit) do |t|
|
|
63
|
+
t.libs << "test"
|
|
64
|
+
t.libs << "lib"
|
|
65
|
+
t.test_files = FileList["test/**/*_test.rb"].reject { |f|
|
|
66
|
+
f.include?("integration") || f.include?("stream_control") ||
|
|
67
|
+
f =~ /quicsilver_test|event_loop_test/
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
Rake::TestTask.new(:test_integration) do |t|
|
|
72
|
+
t.libs << "test"
|
|
73
|
+
t.libs << "lib"
|
|
74
|
+
t.test_files = FileList[
|
|
75
|
+
"test/stream_control_integration_test.rb",
|
|
76
|
+
"test/integration/**/*_test.rb",
|
|
77
|
+
"test/quicsilver_test.rb",
|
|
78
|
+
"test/event_loop_test.rb"
|
|
79
|
+
]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
desc "Run unit and integration tests in parallel"
|
|
83
|
+
task :test_parallel do
|
|
84
|
+
threads = []
|
|
85
|
+
results = {}
|
|
86
|
+
|
|
87
|
+
threads << Thread.new {
|
|
88
|
+
results[:unit] = system("bundle exec rake test_unit 2>&1 > /dev/null")
|
|
89
|
+
}
|
|
90
|
+
threads << Thread.new {
|
|
91
|
+
results[:integration] = system("bundle exec rake test_integration 2>&1 > /dev/null")
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
threads.each(&:join)
|
|
95
|
+
unless results.values.all?
|
|
96
|
+
abort "Tests failed: #{results.inspect}"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
namespace :benchmark do
|
|
101
|
+
desc "Run throughput benchmark"
|
|
102
|
+
task :throughput do
|
|
103
|
+
ruby "benchmarks/throughput.rb"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
desc "Run concurrency benchmark"
|
|
107
|
+
task :concurrent do
|
|
108
|
+
ruby "benchmarks/concurrent.rb"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
desc "Run component micro-benchmarks"
|
|
112
|
+
task :components do
|
|
113
|
+
ruby "benchmarks/components.rb"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
desc "Run all benchmarks"
|
|
117
|
+
task :all => [:components, :throughput, :concurrent]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
desc "Run all benchmarks"
|
|
121
|
+
task :benchmark => "benchmark:all"
|
|
122
|
+
|
|
31
123
|
task :default => :test
|