http3 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +92 -0
- data/LICENSE-DEPENDENCIES.txt +2 -2
- data/README.md +124 -8
- data/ext/http3/extconf.rb +8 -5
- data/ext/http3/nghttp3.c +20 -1
- data/ext/http3/nghttp3.h +38 -3
- data/ext/http3/nghttp3_callbacks.c +8 -10
- data/ext/http3/nghttp3_connection.c +378 -206
- data/ext/http3/nghttp3_nv.c +53 -20
- data/ext/http3/nghttp3_qpack.c +66 -22
- data/ext/http3/session.c +429 -55
- data/lib/http3/body_pipe.rb +160 -0
- data/lib/http3/body_stream.rb +120 -0
- data/lib/http3/rack_adapter.rb +220 -38
- data/lib/http3/request.rb +4 -2
- data/lib/http3/server.rb +551 -41
- data/lib/http3/session.rb +614 -66
- data/lib/http3/version.rb +1 -1
- data/lib/http3/worker_pool.rb +71 -0
- data/lib/http3.rb +3 -0
- data/sig/http3/body_pipe.rbs +45 -0
- data/sig/http3/body_stream.rbs +41 -0
- data/sig/http3/nghttp3/connection.rbs +43 -9
- data/sig/http3/nghttp3/nv.rbs +4 -0
- data/sig/http3/nghttp3/qpack.rbs +10 -3
- data/sig/http3/rack_adapter.rbs +29 -4
- data/sig/http3/request.rbs +5 -3
- data/sig/http3/response.rbs +2 -2
- data/sig/http3/server.rbs +14 -1
- data/sig/http3/session.rbs +136 -13
- data/sig/http3/worker_pool.rbs +19 -0
- data/sig/http3.rbs +3 -0
- metadata +9 -6
- data/.clang-format +0 -1
- data/.vscode/extensions.json +0 -6
- data/.vscode/settings.json +0 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0ef6daf3337a94169eaba8c25785f154b2c06ed104fefaaf4877c1b162b521cf
|
|
4
|
+
data.tar.gz: 24f9d81b87b20b5ba7f18e0551a2ecdb252eb7ee9bca5220a421c1a57cc6f652
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4d13a1693067a9e03f0d197423d21b10ff9115ebc584a2c848db43e3bda958d6382120ba78251f62bed5d71932b081c3a9e66ba7f3c404a602b0d69fbc2f7781
|
|
7
|
+
data.tar.gz: a6318fbe24ec6d23c630c811c6ac42a010996dac9db39e630d6db1a61b14bca79661558660dffd190aed875a312aa73654fe02fa905395834280ee54b866df4a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,95 @@
|
|
|
1
|
+
## [0.0.5] - 2026-11-07
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- `HTTP3::Server` takes `request_body_buffer_size` (64 KiB): a request body that grows past it goes, all of it, to an unnamed temporary file in `Dir.tmpdir` (O_TMPFILE, or a file with a random name unlinked at once where the file system does not take that), which the application reads as `rack.input`. `max_connection_request_buffer` and `max_total_request_buffer` count only the bodies in memory. Where `/tmp` is a tmpfs, the files are in memory too; set `TMPDIR` to a directory on disk. ([#22](https://github.com/unasuke/http3-ruby/pull/22))
|
|
5
|
+
- `HTTP3::Server` takes `max_connection_request_spool` (16 MiB) and `max_total_request_spool` (1 GiB), the bytes of request bodies in temporary files on one connection and over all connections, and `max_connection_request_spool_files` (16) and `max_total_request_spool_files` (256), the files open at once. A request that would pass one of them gets 503, with neither headers nor body. A temporary file stays open, and counts with its bytes, until the response body is done, so that an `each` body may read `rack.input` as it goes. ([#22](https://github.com/unasuke/http3-ruby/pull/22))
|
|
6
|
+
- A request whose temporary file cannot be made or written (`ENOSPC`, `EMFILE`, `EFBIG`, ...) gets 503, and the failure is logged as `request_spool_failed remote=... error=... message=...`, at most once per `refusal_log_interval`. ([#22](https://github.com/unasuke/http3-ruby/pull/22))
|
|
7
|
+
- `HTTP3::Session.attach` takes `request_body_buffer_size:`, `max_connection_request_spool:`, `max_connection_request_spool_files:` and `shared_spool:`, made with `HTTP3::Session.shared_spool(limit, files:)`. `HTTP3::Session#take_spool_error` returns the first failure to make or write a temporary file since the last call. ([#22](https://github.com/unasuke/http3-ruby/pull/22))
|
|
8
|
+
- `HTTP3::Server` takes `threads` (3): the application is called on that many worker threads, an `HTTP3::WorkerPool` whose threads are named `http3-worker-N` and run at the least priority, -3 (`HTTP3::WorkerPool::WORKER_PRIORITY`). `HTTP3::Server.new` raises `ArgumentError` unless it is a positive Integer. ([#24](https://github.com/unasuke/http3-ruby/pull/24))
|
|
9
|
+
- `HTTP3::Body::Pipe`, through which a worker hands an `each` body to the thread that drives the connections, up to `send_buffer_size` ahead, and `HTTP3::StreamClosed`, an `IOError` that its writes raise once the stream has gone away or the server has stopped. An `each` body that gets it stops there, without an error. ([#24](https://github.com/unasuke/http3-ruby/pull/24))
|
|
10
|
+
- `HTTP3::Session#take_refusals` returns the requests refused at once with their statuses, `#next_request` hands out the next request for the application, `#finish(stream_id)` gives back what a request handed out holds, and `#answerable?(stream_id)` tells whether its response can still be sent. ([#24](https://github.com/unasuke/http3-ruby/pull/24))
|
|
11
|
+
- When it stops, `HTTP3::Server` cancels the bodies the workers are writing, waits a second for the workers, and logs those still in the application as `workers_still_running count=N`. ([#24](https://github.com/unasuke/http3-ruby/pull/24))
|
|
12
|
+
- `test/interop/slow_requests.rb`, a check run by hand that a worker busy on the CPU holds up the other connections only briefly: with two workers busy, it measures the round trips of a small request and new handshakes, and fails when one takes over `--max-latency` (0.25 seconds). ([#24](https://github.com/unasuke/http3-ruby/pull/24))
|
|
13
|
+
- Streaming response bodies, which respond to `call` and not `each`: a worker calls one with an `HTTP3::Body::Stream`, and what it writes is sent as it writes it. The stream's `write`, `<<` and `flush` go to the response's `HTTP3::Body::Pipe`, as the chunks of an `each` body do, and its `read` reads the request body, received in full. The body ends when `call` returns, whether or not it closed the stream. A body that writes past `content-length`, or ends short of it, has its stream aborted with H3_INTERNAL_ERROR, and once the client has reset the stream, `write` raises `HTTP3::StreamClosed`. HEAD, 204 and 304 responses do not call it. A worker stays with the body for as long as `call` runs, so no more than `threads` long-lived responses, such as server-sent events, are sent at once. ([#25](https://github.com/unasuke/http3-ruby/pull/25))
|
|
14
|
+
- `write` raises `HTTP3::StreamClosed` once the stream has gone away or the server has stopped, as the writes of an `each` body's pipe do, not only once the client has reset the stream: also on the client's STOP_SENDING and at the end of the connection, and, since [#26](https://github.com/unasuke/http3-ruby/pull/26), once the response has been aborted after `response_timeout`.
|
|
15
|
+
- `HTTP3::Server` takes `response_timeout` (10 seconds): a response whose body has waited on its client that long, with nothing moving, is aborted with H3_REQUEST_CANCELLED (RFC 9114 §4.1.1), and a worker writing it gets `HTTP3::StreamClosed` from the write it waits in, or its next, which ends the body without an error. Moving is the client acknowledging some of the body or widening the stream's flow control window; unless that window holds the body back, an acknowledgement of any body on the connection counts too, since nghttp3 sends the responses of one priority one at a time. It bounds a stall, not the whole response: one its client keeps reading, however slowly, goes on, and one with nothing to send, such as server-sent events between events, never times out. A QUIC client widens its window in steps, so one that reads slowly, such as a video player, or pauses may have its response aborted. `nil` sets no limit; anything else but a positive number makes `HTTP3::Server.new` raise `ArgumentError`. ([#26](https://github.com/unasuke/http3-ruby/pull/26))
|
|
16
|
+
- `HTTP3::Session.attach` takes `response_timeout:` (nil, no limit, by default). `HTTP3::Session#expire_responses(now)` aborts the responses past their time and returns how many, `#next_response_deadline` tells when the next one runs out, and `#response_stalled_since(stream_id)` tells since when a response has waited on its client. ([#26](https://github.com/unasuke/http3-ruby/pull/26))
|
|
17
|
+
- `HTTP3::Server` counts the responses aborted after `response_timeout` with the refusals, and logs them after the `requests_refused` lines, `refusal_log_interval` seconds after the first of either and when `#run` ends, as a warn line such as `responses_timed_out count=3 seconds=60`. ([#26](https://github.com/unasuke/http3-ruby/pull/26))
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- `HTTP3::Request#body`, a binary String, is now `#input`, a binary IO at its start: a `StringIO`, or the temporary `File` of a body past `request_body_buffer_size`. `HTTP3::RackAdapter` puts it in `rack.input` as it is, instead of a copy. ([#22](https://github.com/unasuke/http3-ruby/pull/22))
|
|
21
|
+
- `HTTP3::Server.new` raises `ArgumentError` unless the five new options are positive Integers, `max_total_request_spool` is at least `max_connection_request_spool` and `max_total_request_spool_files` at least `max_connection_request_spool_files`. When a body may go to a temporary file (`max_request_body` over `request_body_buffer_size`), `max_connection_request_buffer` has to be at least `request_body_buffer_size + max_field_section_size` instead of `max_request_body + max_field_section_size`, and `max_connection_request_spool` at least `max_request_body`. ([#22](https://github.com/unasuke/http3-ruby/pull/22))
|
|
22
|
+
- When a body may go to a temporary file, `HTTP3::Server.new` makes one in `Dir.tmpdir` and raises what that raises, such as `Errno::EACCES`. ([#22](https://github.com/unasuke/http3-ruby/pull/22))
|
|
23
|
+
- The application, with its body's `each` and `close`, runs on the worker threads instead of the thread that drives every connection, so a slow request or a slow `each` body no longer holds up the other connections. That thread hands the requests to the free workers, a connection at a time. A worker stays with a request until its body is done, so a client that reads its response slowly, or not at all, keeps a worker, with no time limit. ([#24](https://github.com/unasuke/http3-ruby/pull/24))
|
|
24
|
+
- Since [#26](https://github.com/unasuke/http3-ruby/pull/26), a response whose client stops reading is aborted after `response_timeout`, and the write its body waits in, or its next one, raises `HTTP3::StreamClosed`, which ends the body and frees the worker; one whose client keeps reading, however slowly, keeps it.
|
|
25
|
+
- A request that waits `request_timeout` for a worker gets 503, with neither headers nor body. One that the client resets while it waits never reaches the application, and its stream is reset with H3_REQUEST_CANCELLED. ([#24](https://github.com/unasuke/http3-ruby/pull/24))
|
|
26
|
+
- `HTTP3::Session#take_requests` is gone, in favour of `#take_refusals` and `#next_request`. The Session no longer closes `Request#input`: whoever takes the request closes it and calls `#finish`. Until then, even after the stream or the connection has closed, the request counts for the limits, both the bytes it holds in memory, which came back when it was handed out, and its temporary file, which counted until its response body was done. ([#24](https://github.com/unasuke/http3-ruby/pull/24))
|
|
27
|
+
- `HTTP3::RackAdapter.new` takes `pipe_capacity:` and `wake:`, and `#call` returns `[response, fill]`, where `fill` is nil or a callable that writes an `each` body to its `HTTP3::Body::Pipe`. The Rack body of a `to_path` response is closed on the worker, before the file is sent. ([#24](https://github.com/unasuke/http3-ruby/pull/24))
|
|
28
|
+
- A body that responds to `call` and not `each` is sent as a streaming body instead of being answered with 500; one that responds to neither still gets 500. `HTTP3::RackAdapter#call` returns a `fill` for it, as for an `each` body, and `fill` returns an exception the body raised that its `HTTP3::Body::Pipe` did not take, or nil. ([#25](https://github.com/unasuke/http3-ruby/pull/25))
|
|
29
|
+
- `HTTP3::Body::Pipe#fail` returns true once its error has been taken, and false when it never will be: the body had ended, or the server had given up on it. An exception an `each` or streaming body raises once its stream can no longer be aborted, after the body ended or the client reset the stream, is logged as `app_error` instead of going nowhere. ([#25](https://github.com/unasuke/http3-ruby/pull/25))
|
|
30
|
+
- Not only after the client reset the stream: also after its STOP_SENDING or the end of the connection, and, since [#26](https://github.com/unasuke/http3-ruby/pull/26), once the response has been aborted after `response_timeout`.
|
|
31
|
+
- A client that stops reading a response no longer keeps the worker writing it, the response's send buffer room, or a file body's descriptor, with no time limit: they are let go `response_timeout` after the response stalled. ([#26](https://github.com/unasuke/http3-ruby/pull/26))
|
|
32
|
+
- `HTTP3::Server.new` raises `ArgumentError` unless `response_timeout` is less than `request_timeout` (when neither is `nil`), so that the workers are freed before the requests waiting for them get 503. A configuration that sets only `request_timeout`, to 10 seconds or less, has to set `response_timeout` too. ([#26](https://github.com/unasuke/http3-ruby/pull/26))
|
|
33
|
+
- `HTTP3::Session#abort_stream` lets go at once of what nghttp3 has not taken of the body, so that `#unsent_bytes` and `#unsent_total` no longer count it, and closes a file body's descriptor, instead of keeping them until the client acknowledges the reset. `#push_chunk`, `#push_eof` and `#attach_file` raise `HTTP3::Nghttp3::InvalidStateError` on an aborted stream. ([#26](https://github.com/unasuke/http3-ruby/pull/26))
|
|
34
|
+
- `test/interop/stall_responses.rb` takes `--response-timeout` (10 seconds, 5 to 15) and expects the responses not read to be aborted and logged; `test/interop/server.rb` takes `--response-timeout` too, and `hold_requests.rb` passes it half of `--request-timeout`. ([#26](https://github.com/unasuke/http3-ruby/pull/26))
|
|
35
|
+
- The quic gem 0.0.6 or later is required (`quic/ext.h` ABI 1.1, with ngtcp2 1.26.0): its stream handler tells of a client's STOP_SENDING as it arrives. ([#27](https://github.com/unasuke/http3-ruby/pull/27))
|
|
36
|
+
- A client's STOP_SENDING is acted on as it arrives, instead of when the stream closes, a round trip or more later: a request still coming in, or waiting for a worker, never reaches the application, and the server reads no more of it (STOP_SENDING with H3_REQUEST_CANCELLED); a refused one is not answered; a response being sent stops, its worker getting `HTTP3::StreamClosed`, and what of it was not sent, with a file body's descriptor, is let go at once. The response to a request the application is working on is dropped, as on a reset. ([#27](https://github.com/unasuke/http3-ruby/pull/27))
|
|
37
|
+
- STOP_SENDING on the server's control or QPACK stream closes the connection with H3_CLOSED_CRITICAL_STREAM as it arrives, instead of once the stream has closed. ([#27](https://github.com/unasuke/http3-ruby/pull/27))
|
|
38
|
+
- nghttp3 is now built from 1.19.0 (was 1.13.1). ([#28](https://github.com/unasuke/http3-ruby/pull/28))
|
|
39
|
+
- From nghttp3 1.18.0, a request with a field whose Huffman-coded name nghttp3 estimates over 256 bytes once decoded, or whose value over 64 KiB (the estimate is 8/5 of the coded length), fails the connection with H3_INTERNAL_ERROR: nghttp3 refuses to decode it. Before, such a field reached the Session, which answered 431 if the header section was over `max_field_section_size`, and passed the request on otherwise. A header section over `max_field_section_size` made of smaller fields still gets 431. ([#28](https://github.com/unasuke/http3-ruby/pull/28))
|
|
40
|
+
- `HTTP3::Nghttp3::Callbacks#on_stream_close` is also called for a unidirectional stream of an unknown type that nghttp3 drops (nghttp3 1.16.0). ([#28](https://github.com/unasuke/http3-ruby/pull/28))
|
|
41
|
+
- The hash `HTTP3::Nghttp3::Callbacks#on_recv_settings` yields no longer has `:qpack_encoder_max_dtable_capacity`, which is not a setting the peer sends. ([#28](https://github.com/unasuke/http3-ruby/pull/28))
|
|
42
|
+
- A request the client resets once a worker has taken it, sent in full, has the server's side of its stream reset with H3_REQUEST_CANCELLED, as one reset while it waits for a worker does, so that the stream closes without waiting for the application; its response is dropped as before. Before, the stream stayed open until the connection ended, and a server stopping waited `shutdown_timeout` for it. ([#29](https://github.com/unasuke/http3-ruby/pull/29))
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
- The descriptor of a file body is closed, and what nghttp3 has not taken of a body let go of, once the server lets go of the closed connection (after its closing or draining period), instead of when the garbage collector frees its `HTTP3::Session`: `response_timeout` stops counting then. A client that opened connections and closed them while their file responses went unread could make the server run out of descriptors, after which temporary files, file responses and the application's own `File.open` failed with `EMFILE`. `HTTP3::Session#release_all` does it, and deletes the nghttp3 connection: afterwards `#submit_response` and `#stop_reading` raise `HTTP3::Nghttp3::InvalidStateError`, as on a failed connection, and `#push_chunk`, `#push_eof` and `#attach_file` as on an aborted stream. ([#29](https://github.com/unasuke/http3-ruby/pull/29))
|
|
46
|
+
|
|
47
|
+
## [0.0.4] - 2026-10-27
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
- quic-interop-runner's http3 test case runs against http3-ruby with `test/interop/run_interop_runner.sh`. The runner's ns-3 network simulator passes no packets under rootless podman, so the script runs the runner on `test/interop/quic-interop-runner.yml`, its compose with a stand-in (`test/interop/sim/`) that forwards with plain Linux routing; it makes the networks afresh before and after each run. CI runs it against the clients of ngtcp2, quic-go, quiche, neqo, aioquic, picoquic, lsquic, s2n-quic and quinn. ([#10](https://github.com/unasuke/http3-ruby/pull/10))
|
|
51
|
+
- `HTTP3::Server` takes `max_connection_request_buffer` (4 MiB) and `max_total_request_buffer` (64 MiB): the bytes that requests not yet passed to the application may hold on one connection and over all connections, header fields counted as RFC 9114 §4.2.2 counts them. A request that would pass either is answered with 503, with neither headers nor body. `HTTP3::Session.attach` takes the same limits as `max_connection_request_buffer:` and `shared_buffer:`, made with `HTTP3::Session.shared_buffer(limit)`. ([#12](https://github.com/unasuke/http3-ruby/pull/12))
|
|
52
|
+
- `test/interop/hold_requests.rb`, a check run by hand that requests held open cannot exhaust the server's memory: 20 connections of 100 requests, none of them ever finished, then a GET on a new connection. ([#12](https://github.com/unasuke/http3-ruby/pull/12))
|
|
53
|
+
- `HTTP3::Server` takes `max_connection_send_buffer` (1 MiB) and `max_total_send_buffer` (64 MiB): the bytes of array and `each` response bodies that may be queued but unsent on one connection and over all connections. A body waits for room, and while the total is used up, the bodies on every connection wait. `HTTP3::Session#fill_bodies` and `#wants_fill?` take the room left as `connection_limit:` and `room:`, and `#unsent_total` tells what a Session has unsent. ([#17](https://github.com/unasuke/http3-ruby/pull/17))
|
|
54
|
+
- `HTTP3::Server` takes `request_timeout` (30 seconds): the time a request may take to come in full, counted from the start of its headers. One that takes longer is answered with 408, with neither headers nor body, and the rest of it is not read. `nil` sets no limit; anything else but a positive number makes `HTTP3::Server.new` raise `ArgumentError`. `HTTP3::Session.attach` takes it as `request_timeout:`, `HTTP3::Session#expire_requests(now)` refuses the requests past their time, and `#next_request_deadline` tells when the next one runs out. 30 seconds fit the default `max_request_body` of 1 MiB at about 280 kbps; an application that raises `max_request_body` should lengthen `request_timeout` to match or set it to `nil`, or large uploads over slow links get 408 (100 MiB in 30 seconds takes about 28 Mbps). ([#18](https://github.com/unasuke/http3-ruby/pull/18))
|
|
55
|
+
- `HTTP3::Server` logs how many requests it answered without the application (400, 408, 413, 431, 501 and 503), which are not logged one by one: `refusal_log_interval` seconds (60) after the first of them, and when `#run` ends, a warn line per status such as `requests_refused status=503 count=1234 seconds=60`, with the seconds since the first. `HTTP3::Server.new` raises `ArgumentError` for a `refusal_log_interval` that is not a positive number. `HTTP3::RackAdapter#take_refusals` returns the 400s and 501s it answered since the last call, as `{status => count}`. ([#19](https://github.com/unasuke/http3-ruby/pull/19))
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
- The interop checks in CI, curl and quic-interop-runner, run on every push to main as well as weekly. ([#10](https://github.com/unasuke/http3-ruby/pull/10))
|
|
59
|
+
- http3 depends on quic `~> 0.0.5`. A client on that quic keeps its connection when the server stops reading a request it is still sending, and closes the qlog file of a connection once the connection ends. ([#11](https://github.com/unasuke/http3-ruby/pull/11))
|
|
60
|
+
- 413 and 431 are answered as soon as the request passes the limit, before the rest of it arrives. The server does not read the rest: `HTTP3::Session#stop_reading` sends STOP_SENDING with H3_NO_ERROR, as RFC 9114 §4.1 allows. ([#12](https://github.com/unasuke/http3-ruby/pull/12))
|
|
61
|
+
- `HTTP3::Session#take_requests` hands out a request it refuses (413, 431 or 503) as soon as it is refused, with neither headers nor body. ([#12](https://github.com/unasuke/http3-ruby/pull/12))
|
|
62
|
+
- `HTTP3::Session#respond` raises `HTTP3::Nghttp3::InvalidStateError` for a request already answered, instead of sending a second response. ([#12](https://github.com/unasuke/http3-ruby/pull/12))
|
|
63
|
+
- The request that `HTTP3::Session#fill_bodies` passes to its block has only the pseudo-header fields, and no body: a response being sent no longer keeps the body of its request. ([#12](https://github.com/unasuke/http3-ruby/pull/12))
|
|
64
|
+
- `HTTP3::Server.new` raises `ArgumentError` for request limits that are not Integers in range, and for a `max_connection_request_buffer` smaller than `max_request_body + max_field_section_size` or a `max_total_request_buffer` smaller than `max_connection_request_buffer`, which would answer a request within both limits with 503 on an idle server. ([#12](https://github.com/unasuke/http3-ruby/pull/12))
|
|
65
|
+
- `HTTP3::Server.new` raises `ArgumentError` unless `send_buffer_size`, `max_connection_send_buffer` and `max_total_send_buffer` are positive Integers, each at least the one before. ([#17](https://github.com/unasuke/http3-ruby/pull/17))
|
|
66
|
+
- `HTTP3::Nghttp3::NV_FLAG_NO_COPY_NAME` and `NV_FLAG_NO_COPY_VALUE` are ignored: nghttp3 always copies the names and values, and lowercases the names it copies. `HTTP3::Nghttp3::NV.new` raises `ArgumentError` for flags that are not an Integer of 0..255. ([#13](https://github.com/unasuke/http3-ruby/pull/13))
|
|
67
|
+
- A callback may call back into its `HTTP3::Nghttp3::Connection` only as far as nghttp3 allows: inside the callbacks of `#read_stream`, the `#submit_*` methods, `#resume_stream`, `#block_stream`, `#unblock_stream`, `#shutdown_stream_read`, `#shutdown_stream_write`, `#submit_shutdown_notice`, `#shutdown`, `#set_max_client_streams_bidi` and `#stream_writable?`; inside any other callback, nothing that calls into nghttp3. The rest, `#close` among them, raise `InvalidStateError`, as do `HTTP3::Session`'s methods called from inside its callbacks. Once nghttp3 reports a connection error, only `#close` is allowed. ([#13](https://github.com/unasuke/http3-ruby/pull/13))
|
|
68
|
+
- What nghttp3 asserts of its callers raises instead. `ArgumentError`: a stream id out of 0..2**62-1 or of the wrong kind for the call, a smaller `#set_max_client_streams_bidi` than before, and a header that is not an `HTTP3::Nghttp3::NV` with String name and value, whose `to_str` is no longer called. `InvalidStateError`: `#submit_info` or `#set_max_client_streams_bidi` on a client, a `#submit_*` before `#bind_qpack_streams`, and a GOAWAY before `#bind_control_stream` or after `#shutdown`. ([#13](https://github.com/unasuke/http3-ruby/pull/13))
|
|
69
|
+
- `HTTP3::RackAdapter` answers 400, without calling the application or logging, a request whose scheme is not http or https, whose path has a fragment, whose authority is not `host` or `host:port`, or whose `:authority` and `host` field differ (RFC 9114 §4.3.1). It answers CONNECT with 501, since the server opens no tunnels. ([#14](https://github.com/unasuke/http3-ruby/pull/14))
|
|
70
|
+
- A response gets 500 for a status outside 200..599, a `content-length` given more than once or not a number, or one that a file or array body does not have. A body read with `each` is counted as it is sent, and its stream is aborted once it passes `content-length` or ends short of it. ([#14](https://github.com/unasuke/http3-ruby/pull/14))
|
|
71
|
+
- The `content-length` of a 204 response is dropped, as RFC 9110 §8.6 forbids one. ([#14](https://github.com/unasuke/http3-ruby/pull/14))
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
- The browser steps of `test/interop/README.md` work as written. Chrome opens 127.0.0.1, since it looks localhost up as ::1 first. Firefox gets Alt-Svc over TCP from `openssl s_server` on the same port, since `network.http.http3.alt-svc-mapping-for-testing` alone still left it connecting over TCP, where nothing listened. ([#9](https://github.com/unasuke/http3-ruby/pull/9))
|
|
75
|
+
- A client that never finished its requests could make the server hold `max_request_body` on every stream of every connection: about 100 MiB per connection, and 100 GiB in all, with the defaults. The new buffer limits bound it. ([#12](https://github.com/unasuke/http3-ruby/pull/12))
|
|
76
|
+
- A request the client resets before sending it in full gets the server's side of its stream reset with H3_REQUEST_INCOMPLETE (RFC 9114 §4.1), so that the stream closes. Before, it stayed open, holding what had come of the request, until the connection ended. ([#12](https://github.com/unasuke/http3-ruby/pull/12))
|
|
77
|
+
- A client that allows fewer than three unidirectional streams no longer aborts the server process on nghttp3's assertions: its connection is closed with H3_GENERAL_PROTOCOL_ERROR. ([#12](https://github.com/unasuke/http3-ruby/pull/12))
|
|
78
|
+
- Once nghttp3 reports an error on a connection, `HTTP3::Session` deletes the nghttp3 connection and the QUIC connection closes. Before, calls made later in the same turn still went into the failed nghttp3 connection. ([#12](https://github.com/unasuke/http3-ruby/pull/12))
|
|
79
|
+
- An exception from `HTTP3::Session.attach` no longer stops `HTTP3::Server#run`. It is logged as `session_error` and closes that connection only. ([#12](https://github.com/unasuke/http3-ruby/pull/12))
|
|
80
|
+
- Two use-after-frees in `HTTP3::Nghttp3`: a header submitted with the no-copy flags went out as freed memory once its NV was dropped, and `Connection#close` inside a callback freed the nghttp3 connection nghttp3 was still running on. ([#13](https://github.com/unasuke/http3-ruby/pull/13))
|
|
81
|
+
- `Connection#submit_*` and `QPACK::Encoder#encode` put the header list on the heap. On the machine stack, a long list overflowed a thread's stack. ([#13](https://github.com/unasuke/http3-ruby/pull/13))
|
|
82
|
+
- `Connection#submit_*`, `QPACK::Encoder#encode` and `HTTP3::Session#submit_response` take the pointers into the header Strings only once no Ruby code can run before nghttp3 copies them. A GC that compacted the heap inside a later field's `to_str` made them send the bytes left behind, and the body's `to_str` could close the connection before nghttp3 was called with it. `Session#push_chunk` and `#attach_file` likewise convert their arguments before they look up the stream. ([#13](https://github.com/unasuke/http3-ruby/pull/13))
|
|
83
|
+
- A stream id, role or order of calls that nghttp3 asserts against no longer aborts the process. ([#13](https://github.com/unasuke/http3-ruby/pull/13))
|
|
84
|
+
- After nghttp3 reports a connection error, `HTTP3::Nghttp3::Connection` no longer lets calls go into the failed nghttp3 connection. ([#13](https://github.com/unasuke/http3-ruby/pull/13))
|
|
85
|
+
- `HTTP3::Nghttp3::QPACK::Decoder` decodes at most `MAX_STREAMS` (2000) streams at once, and raises `InvalidStateError` for one more. Before, a peer that began field sections and never finished them made it hold any number of stream contexts. ([#13](https://github.com/unasuke/http3-ruby/pull/13))
|
|
86
|
+
- Calling `QPACK::Encoder#initialize` or `QPACK::Decoder#initialize` again frees the old encoder or decoder, which leaked, and a decoder's stream contexts, from which it went on decoding a stream in the old state. ([#13](https://github.com/unasuke/http3-ruby/pull/13))
|
|
87
|
+
- The Rack env no longer carries what the client sent unchecked: the scheme goes in lowercase, and an empty port is the scheme's default. Before, a scheme such as file or ftp became `rack.url_scheme`, a fragment stayed in `PATH_INFO`, an authority such as `example.com:abc` was split into `SERVER_NAME` and `SERVER_PORT` however it fell apart, and `:authority` won over a `host` field that said otherwise. ([#14](https://github.com/unasuke/http3-ruby/pull/14))
|
|
88
|
+
- An array body with a chunk that is not a String is answered with 500 before anything is sent. Before, its headers went out and then its stream was aborted. ([#14](https://github.com/unasuke/http3-ruby/pull/14))
|
|
89
|
+
- A client that never finished its requests could keep what they held of the request buffers for as long as it kept them open: with the defaults, 16 such connections filled `max_total_request_buffer` and left every other request with 503. Such a request now gets 408 once `request_timeout` has passed. ([#18](https://github.com/unasuke/http3-ruby/pull/18))
|
|
90
|
+
- `HTTP3::Nghttp3::Connection#read_stream` takes frozen data, such as a String literal under `frozen_string_literal`. Before, it raised `FrozenError`. ([#16](https://github.com/unasuke/http3-ruby/pull/16))
|
|
91
|
+
- A client that stops reading its responses could make the server hold `send_buffer_size` on every stream of every connection: about 6.4 GiB with the defaults. The new send buffer limits bound it. ([#17](https://github.com/unasuke/http3-ruby/pull/17))
|
|
92
|
+
|
|
1
93
|
## [0.0.3] - 2026-10-13
|
|
2
94
|
|
|
3
95
|
### Added
|
data/LICENSE-DEPENDENCIES.txt
CHANGED
|
@@ -9,7 +9,7 @@ LICENSE-DEPENDENCIES.txt. The http3 gem itself is MIT licensed; see
|
|
|
9
9
|
LICENSE.txt.
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
nghttp3 1.
|
|
12
|
+
nghttp3 1.19.0 -- https://github.com/ngtcp2/nghttp3
|
|
13
13
|
---------------------------------------------------
|
|
14
14
|
|
|
15
15
|
The MIT License
|
|
@@ -39,7 +39,7 @@ Some nghttp3 source files also carry "Copyright (c) ngtcp2 contributors" or
|
|
|
39
39
|
"Copyright (c) nghttp2 contributors" notices, under the same terms.
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
sfparse (bundled in nghttp3 1.
|
|
42
|
+
sfparse (bundled in nghttp3 1.19.0, lib/sfparse) -- https://github.com/ngtcp2/sfparse
|
|
43
43
|
-------------------------------------------------------------------------------------
|
|
44
44
|
|
|
45
45
|
The MIT License
|
data/README.md
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
# http3-ruby
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
> [!WARNING]
|
|
7
|
-
> The code in this repository is almost entirely generated by Claude Code and reviewed by a human.
|
|
3
|
+
[](https://github.com/unasuke/http3-ruby/actions/workflows/main.yml)
|
|
4
|
+
[](https://github.com/unasuke/http3-ruby/actions/workflows/interop.yml)
|
|
8
5
|
|
|
9
6
|
`http3` serves Rack 3 applications over HTTP/3. It runs QUIC through the
|
|
10
7
|
[quic gem](https://github.com/unasuke/quic-ruby) and HTTP/3 through
|
|
@@ -12,6 +9,28 @@
|
|
|
12
9
|
links in statically. The nghttp3 bindings are also there to use directly, as
|
|
13
10
|
`HTTP3::Nghttp3`.
|
|
14
11
|
|
|
12
|
+
It is not suited for production yet:
|
|
13
|
+
|
|
14
|
+
- The application runs on `threads:` worker threads. A worker stays with a
|
|
15
|
+
request until its response body has been generated, so an `each` or
|
|
16
|
+
streaming body keeps the thread for as long as it goes on: no more than
|
|
17
|
+
`threads:` long-lived responses, such as server-sent events, are sent at
|
|
18
|
+
once. A response whose client stops reading is aborted after
|
|
19
|
+
`response_timeout:`, and the write its body waits in, or its next one,
|
|
20
|
+
raises `HTTP3::StreamClosed`, which ends the body and frees the thread;
|
|
21
|
+
one that its client keeps reading, however slowly, keeps the thread. Once
|
|
22
|
+
every worker is taken, other requests wait, and get 503 after
|
|
23
|
+
`request_timeout:`.
|
|
24
|
+
- The workers share Ruby's GVL with the thread that drives every connection,
|
|
25
|
+
so an application busy on the CPU delays packet handling. Ruby 3.4 and
|
|
26
|
+
later can shorten that with `RUBY_THREAD_TIMESLICE`.
|
|
27
|
+
- A request whose client goes away is not interrupted: the application runs
|
|
28
|
+
to its end.
|
|
29
|
+
- CI runs it on Ruby 4.0 alone, not on 3.3, the oldest it installs on. It is
|
|
30
|
+
checked against curl and the clients of quic-interop-runner, over links
|
|
31
|
+
with no loss or delay, and has not been run under load or over lossy
|
|
32
|
+
networks.
|
|
33
|
+
|
|
15
34
|
## Installation
|
|
16
35
|
|
|
17
36
|
Linux only, Ruby 3.3 or later. Like the quic gem, installing builds C code, so
|
|
@@ -56,19 +75,92 @@ Other options, with their defaults:
|
|
|
56
75
|
| Option | Default | |
|
|
57
76
|
| --- | --- | --- |
|
|
58
77
|
| `max_connections:` | `1024` | Connections at once; more are refused with CONNECTION_REFUSED |
|
|
78
|
+
| `threads:` | `3` | Worker threads that call the application |
|
|
59
79
|
| `max_request_body:` | `1_048_576` | Bytes of request body; more gets 413 |
|
|
60
80
|
| `max_field_section_size:` | `65_536` | Bytes of request headers (RFC 9114 §4.2.2); more gets 431 |
|
|
81
|
+
| `max_connection_request_buffer:` | `4_194_304` | Bytes of memory a connection's requests may hold until the application is done with them (headers counted as RFC 9114 §4.2.2 does, and bodies up to `request_body_buffer_size:`); more gets 503 |
|
|
82
|
+
| `max_total_request_buffer:` | `67_108_864` | The same over all connections; more gets 503 |
|
|
83
|
+
| `request_body_buffer_size:` | `65_536` | Bytes of a request body kept in memory; a longer body goes to a temporary file, all of it |
|
|
84
|
+
| `max_connection_request_spool:` | `16_777_216` | Bytes of request bodies a connection may hold in temporary files; more gets 503 |
|
|
85
|
+
| `max_total_request_spool:` | `1_073_741_824` | The same over all connections |
|
|
86
|
+
| `max_connection_request_spool_files:` | `16` | Request bodies a connection may hold in temporary files at once; more gets 503 |
|
|
87
|
+
| `max_total_request_spool_files:` | `256` | The same over all connections |
|
|
61
88
|
| `send_buffer_size:` | `65_536` | Bytes of a response body queued but unsent per stream |
|
|
89
|
+
| `max_connection_send_buffer:` | `1_048_576` | Bytes of array, `each` and streaming response bodies a connection may have queued but unsent; more waits |
|
|
90
|
+
| `max_total_send_buffer:` | `67_108_864` | The same over all connections |
|
|
91
|
+
| `request_timeout:` | `30` | Seconds a request may take to come in full, counted from its headers (later gets 408), and then to wait for a worker (later gets 503). `nil` for no limit |
|
|
92
|
+
| `response_timeout:` | `10` | Seconds a response may stall, waiting on its client with nothing it sent acknowledged, before its stream is aborted (H3_REQUEST_CANCELLED). Not a limit on the whole response, which goes on for as long as its client keeps reading. Less than `request_timeout:`; `nil` for no limit |
|
|
93
|
+
| `refusal_log_interval:` | `60` | Seconds between the `requests_refused` and `responses_timed_out` log lines, which count the requests answered without the application (400, 408, 413, 431, 501, 503) and the responses aborted after `response_timeout:` |
|
|
62
94
|
| `shutdown_timeout:` | `30` | Seconds a graceful stop waits for the requests under way |
|
|
63
95
|
| `qlog_dir:` | `nil` | Directory for one qlog file per connection |
|
|
64
96
|
| `transport_params:` | `QUIC::TransportParams.default` | QUIC transport parameters |
|
|
65
97
|
| `settings:` | `QUIC::Settings.default` | QUIC settings (the ALPN is always `h3`) |
|
|
66
98
|
| `logger:` | `Logger.new($stderr)` | Where failures are logged |
|
|
67
99
|
|
|
100
|
+
A request body over `request_body_buffer_size:` is written to an unnamed
|
|
101
|
+
file in `Dir.tmpdir` (`TMPDIR`, or `/tmp`), on the thread that drives every
|
|
102
|
+
connection. Where `/tmp` is a tmpfs, as on Debian 13 and Fedora, that file
|
|
103
|
+
is in memory; set `TMPDIR` to a directory on disk. A body still has to
|
|
104
|
+
arrive within `request_timeout:`, so raise it together with
|
|
105
|
+
`max_request_body:`.
|
|
106
|
+
|
|
107
|
+
Unlike `request_timeout:`, `response_timeout:` does not bound how long a
|
|
108
|
+
response takes: server-sent events or a large download go on for as long
|
|
109
|
+
as the client keeps reading them. It bounds how long a response may stall.
|
|
110
|
+
It counts, for each response, from the last time its client acknowledged
|
|
111
|
+
some of it or widened its flow control window, while some of it waits to
|
|
112
|
+
be sent; a response with nothing to send, such as server-sent events
|
|
113
|
+
between events, never times out. A QUIC client widens its window in steps,
|
|
114
|
+
so one that reads slowly, such as a video player, or pauses may have its
|
|
115
|
+
response aborted, and browsers then ask for the rest with a Range request;
|
|
116
|
+
for such clients, raise `response_timeout:`, and `request_timeout:` above
|
|
117
|
+
it. While another response on the same connection is being read, those
|
|
118
|
+
waiting their turn behind it do not time out.
|
|
119
|
+
|
|
120
|
+
`max_connection_request_buffer:` has to be at least `max_field_section_size:`
|
|
121
|
+
plus `max_request_body:`, or `request_body_buffer_size:` if that is smaller;
|
|
122
|
+
`max_total_request_buffer:` at least `max_connection_request_buffer:`; when
|
|
123
|
+
a body may go to a file, `max_connection_request_spool:` at least
|
|
124
|
+
`max_request_body:`; `max_total_request_spool:` at least
|
|
125
|
+
`max_connection_request_spool:`; and `max_total_request_spool_files:` at
|
|
126
|
+
least `max_connection_request_spool_files:`. That way a request within the
|
|
127
|
+
limits never gets 503 from an idle server; otherwise `HTTP3::Server.new`
|
|
128
|
+
raises `ArgumentError`. It does too unless `response_timeout:` is less than
|
|
129
|
+
`request_timeout:` (when neither is `nil`), so that the workers clients
|
|
130
|
+
that stop reading keep are freed before the requests waiting for them get
|
|
131
|
+
503. When a body may go to a file, `HTTP3::Server.new` also makes one in
|
|
132
|
+
`Dir.tmpdir`, and raises what that raises.
|
|
133
|
+
|
|
134
|
+
A request gets 413, 431 or 503 as soon as it passes a limit, without waiting
|
|
135
|
+
for the rest of it, and the server reads no more of it (STOP_SENDING with
|
|
136
|
+
H3_NO_ERROR). The 503 has neither headers nor body.
|
|
137
|
+
|
|
138
|
+
A request whose client asks for no more of its response (STOP_SENDING)
|
|
139
|
+
before a worker takes it never reaches the application, and the server
|
|
140
|
+
reads no more of it.
|
|
141
|
+
|
|
142
|
+
A request gets 400, without the application seeing it, when its scheme is
|
|
143
|
+
not `http` or `https`, its path has a fragment, its authority is not `host`
|
|
144
|
+
or `host:port`, or its `:authority` and `host` field differ. `CONNECT` gets
|
|
145
|
+
501: the server opens no tunnels.
|
|
146
|
+
|
|
68
147
|
Response bodies may be files (`to_path`, sent 16 KiB at a time as the
|
|
69
|
-
connection takes them), arrays
|
|
70
|
-
(`call`)
|
|
71
|
-
|
|
148
|
+
connection takes them), arrays, anything with `each`, or streaming bodies
|
|
149
|
+
(`call`). A streaming body is called with a stream whose `read` reads the
|
|
150
|
+
request body, received in full; the body ends when `call` returns, so
|
|
151
|
+
writing to the stream from another thread afterwards is not supported. Its
|
|
152
|
+
stream is aborted (H3_INTERNAL_ERROR) once what it writes goes past
|
|
153
|
+
`content-length` or ends short of it, and `write` raises
|
|
154
|
+
`HTTP3::StreamClosed` (an `IOError`) once the client has reset the stream
|
|
155
|
+
or asked for no more of it (STOP_SENDING), or the response has been aborted
|
|
156
|
+
after `response_timeout:`.
|
|
157
|
+
These get 500: a status outside 200..599; response headers whose names are not lowercase tokens or whose
|
|
158
|
+
values contain NUL, CR or LF; a `content-length` given more than once, not a
|
|
159
|
+
number, or not the length of a file or array body; and an array body with a
|
|
160
|
+
chunk that is not a String. A body read with `each` is counted as it is
|
|
161
|
+
sent, and its stream is aborted (H3_INTERNAL_ERROR) once it goes past
|
|
162
|
+
`content-length` or ends short of it. The `content-length` of a 204 is
|
|
163
|
+
dropped.
|
|
72
164
|
|
|
73
165
|
### Signals
|
|
74
166
|
|
|
@@ -105,6 +197,30 @@ alt-svc: h3=":443"
|
|
|
105
197
|
|
|
106
198
|
That is configured in that server (nginx, Puma, ...), not in this gem.
|
|
107
199
|
|
|
200
|
+
### HTTP3::Nghttp3
|
|
201
|
+
|
|
202
|
+
The bindings keep nghttp3's rules for its callers, so that a wrong call
|
|
203
|
+
raises instead of crashing the process or reaching freed memory:
|
|
204
|
+
|
|
205
|
+
- A callback (a `Callbacks` block or a body block) runs while nghttp3 is in
|
|
206
|
+
the middle of a call. Inside the callbacks of `read_stream`, it may call
|
|
207
|
+
`submit_*`, `resume_stream`, `block_stream`, `unblock_stream`,
|
|
208
|
+
`shutdown_stream_read`, `shutdown_stream_write`, `submit_shutdown_notice`,
|
|
209
|
+
`shutdown`, `set_max_client_streams_bidi` and `stream_writable?` on the
|
|
210
|
+
same connection. `close`, `read_stream`, `writev_stream` and the rest
|
|
211
|
+
raise `InvalidStateError` there, and inside any other callback every call
|
|
212
|
+
into nghttp3 does.
|
|
213
|
+
- Once nghttp3 reports a connection error, only `#close` can be called; the
|
|
214
|
+
rest raise `InvalidStateError`.
|
|
215
|
+
- `NV_FLAG_NO_COPY_NAME` and `NV_FLAG_NO_COPY_VALUE` are ignored: names and
|
|
216
|
+
values are always copied.
|
|
217
|
+
- Stream ids are checked for their range and kind, and calls for the role
|
|
218
|
+
and state of the connection, raising `ArgumentError` and
|
|
219
|
+
`InvalidStateError` instead of tripping nghttp3's assertions.
|
|
220
|
+
- A `QPACK::Decoder` decodes at most `MAX_STREAMS` (2000) streams at once.
|
|
221
|
+
|
|
222
|
+
The signatures in `sig/` describe each method.
|
|
223
|
+
|
|
108
224
|
## Development
|
|
109
225
|
|
|
110
226
|
After checking out the repo, run `bin/setup` to install dependencies. Then,
|
data/ext/http3/extconf.rb
CHANGED
|
@@ -5,10 +5,11 @@ require "mini_portile2"
|
|
|
5
5
|
|
|
6
6
|
# nghttp3 is downloaded and built at install time and linked statically, so
|
|
7
7
|
# installing needs no system package and always gets this version.
|
|
8
|
-
#
|
|
9
|
-
# but libc, so its version does not have
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
# The const getters such as nghttp3_conn_is_stream_writable2 need 1.16.0 or
|
|
9
|
+
# later. nghttp3 depends on nothing but libc, so its version does not have
|
|
10
|
+
# to follow the quic gem's ngtcp2.
|
|
11
|
+
NGHTTP3_VERSION = "1.19.0"
|
|
12
|
+
NGHTTP3_SHA256 = "0dc9a55d38ca04c282524bfecdc5e31d393a4bb430200263dd06a87801a150af"
|
|
12
13
|
|
|
13
14
|
nghttp3 = MiniPortile.new("nghttp3", NGHTTP3_VERSION)
|
|
14
15
|
nghttp3.files = [{
|
|
@@ -35,7 +36,9 @@ abort "libnghttp3 not linkable" unless have_func("nghttp3_version", "nghttp3/ngh
|
|
|
35
36
|
# looked up at load time with rb_ext_resolve_symbol (Ruby 3.3 or later).
|
|
36
37
|
require "quic/ext_include"
|
|
37
38
|
append_cppflags("-I#{QUIC::EXT_INCLUDE_DIR}")
|
|
38
|
-
abort "quic/ext.h not found (the quic gem 0.0.
|
|
39
|
+
abort "quic/ext.h not found (the quic gem 0.0.6 or later provides it)" unless have_header("quic/ext.h")
|
|
40
|
+
abort "quic/ext.h lacks recv_stop_sending (the quic gem 0.0.6 or later provides it)" unless
|
|
41
|
+
have_struct_member("quic_ext_stream_handler", "recv_stop_sending", "quic/ext.h")
|
|
39
42
|
abort "rb_ext_resolve_symbol not found (Ruby 3.3 or later is required)" unless
|
|
40
43
|
have_func("rb_ext_resolve_symbol", "ruby.h")
|
|
41
44
|
|
data/ext/http3/nghttp3.c
CHANGED
|
@@ -136,6 +136,25 @@ void nghttp3_rb_raise(int error_code, const char *fmt, ...) {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
/* The largest stream id, the largest value of a QUIC variable-length
|
|
140
|
+
* integer. */
|
|
141
|
+
#define NGHTTP3_RB_MAX_STREAM_ID ((1ULL << 62) - 1)
|
|
142
|
+
|
|
143
|
+
int64_t nghttp3_rb_stream_id(VALUE rb_id) {
|
|
144
|
+
VALUE id = rb_to_int(rb_id);
|
|
145
|
+
uint64_t value;
|
|
146
|
+
/* 0 for zero, 1 for a positive value that fits, -1 or -2 for a negative
|
|
147
|
+
* one and 2 for one too large. */
|
|
148
|
+
int sign = rb_integer_pack(id, &value, 1, sizeof(value), 0,
|
|
149
|
+
INTEGER_PACK_MSWORD_FIRST |
|
|
150
|
+
INTEGER_PACK_NATIVE_BYTE_ORDER);
|
|
151
|
+
if ((sign == 0 || sign == 1) && value <= NGHTTP3_RB_MAX_STREAM_ID) {
|
|
152
|
+
return (int64_t)value;
|
|
153
|
+
}
|
|
154
|
+
rb_raise(rb_eArgError, "stream_id must be 0..2**62-1 (got %" PRIsVALUE ")",
|
|
155
|
+
rb_id);
|
|
156
|
+
}
|
|
157
|
+
|
|
139
158
|
/*
|
|
140
159
|
* call-seq:
|
|
141
160
|
* Nghttp3.err_is_fatal?(error_code) -> true or false
|
|
@@ -174,7 +193,7 @@ static const quic_ext_api *http3_load_quic_api(uint32_t major) {
|
|
|
174
193
|
VALUE version = rb_const_get(quic, rb_intern("VERSION"));
|
|
175
194
|
rb_raise(rb_eLoadError,
|
|
176
195
|
"http3 needs the quic gem's C function table for quic/ext.h "
|
|
177
|
-
"ABI major version %u (quic ~> 0.0.
|
|
196
|
+
"ABI major version %u (quic ~> 0.0.6), which quic %" PRIsVALUE
|
|
178
197
|
" does not provide",
|
|
179
198
|
(unsigned int)major, version);
|
|
180
199
|
}
|
data/ext/http3/nghttp3.h
CHANGED
|
@@ -46,6 +46,13 @@ extern const rb_data_type_t settings_data_type;
|
|
|
46
46
|
/* TypedData type for Callbacks */
|
|
47
47
|
extern const rb_data_type_t callbacks_data_type;
|
|
48
48
|
|
|
49
|
+
/* Which call into nghttp3 a connection is in, for ConnectionObj.entered.
|
|
50
|
+
* nghttp3 runs callbacks only inside such a call, and lets a callback call
|
|
51
|
+
* back into the connection only as far as that call allows: from inside
|
|
52
|
+
* read_stream, ngtcp2's examples submit a response, but nghttp3 is in no
|
|
53
|
+
* state to read, write or be deleted. */
|
|
54
|
+
enum { CONN_IDLE, CONN_IN_READ, CONN_IN_OTHER };
|
|
55
|
+
|
|
49
56
|
/* The C side of an Nghttp3::Connection. nghttp3 gets a pointer to this
|
|
50
57
|
* struct as conn_user_data, never the Ruby object: GC.compact may move the
|
|
51
58
|
* object, and dcompact keeps self pointing at it. */
|
|
@@ -66,6 +73,19 @@ typedef struct {
|
|
|
66
73
|
VALUE pending_exc;
|
|
67
74
|
int is_closed;
|
|
68
75
|
int is_server;
|
|
76
|
+
int entered; /* CONN_IDLE, or the call into nghttp3 under way */
|
|
77
|
+
/* nghttp3 returned an error after which it allows nothing but
|
|
78
|
+
* nghttp3_conn_del: any from read_stream or writev_stream, or a fatal one
|
|
79
|
+
* from another call. */
|
|
80
|
+
int failed;
|
|
81
|
+
/* What has been done to the connection, for the calls nghttp3 asserts
|
|
82
|
+
* come after it or not after it. goaway is 0 before any GOAWAY, 1 after
|
|
83
|
+
* submit_shutdown_notice and 2 after shutdown. max_client_streams is the
|
|
84
|
+
* last value given to set_max_client_streams_bidi. */
|
|
85
|
+
int ctrl_bound;
|
|
86
|
+
int qpack_bound;
|
|
87
|
+
int goaway;
|
|
88
|
+
uint64_t max_client_streams;
|
|
69
89
|
} ConnectionObj;
|
|
70
90
|
|
|
71
91
|
/* Run body(arg) under rb_protect on behalf of a callback of obj. An
|
|
@@ -82,13 +102,28 @@ void nghttp3_rb_conn_stream_closed(ConnectionObj *obj, int64_t stream_id);
|
|
|
82
102
|
|
|
83
103
|
/* Helper functions */
|
|
84
104
|
VALUE nghttp3_rb_error_class_for_code(int error_code);
|
|
85
|
-
void nghttp3_rb_raise(int error_code, const char *fmt,
|
|
105
|
+
NORETURN(PRINTF_ARGS(void nghttp3_rb_raise(int error_code, const char *fmt,
|
|
106
|
+
...),
|
|
107
|
+
2, 3));
|
|
108
|
+
|
|
109
|
+
/* rb_id (to_int is called) as a stream id for nghttp3, which asserts the
|
|
110
|
+
* range of the ids it is given: ArgumentError unless it is 0..2**62-1. */
|
|
111
|
+
int64_t nghttp3_rb_stream_id(VALUE rb_id);
|
|
86
112
|
|
|
87
113
|
/* Settings helper */
|
|
88
114
|
nghttp3_settings *nghttp3_rb_get_settings(VALUE rb_settings);
|
|
89
115
|
|
|
90
|
-
/* NV
|
|
91
|
-
|
|
116
|
+
/* NV helpers. nghttp3_rb_nva_fill fills nva with the first nvlen elements
|
|
117
|
+
* of headers, an Array of NVs, pointing into their Strings. It never raises,
|
|
118
|
+
* allocates nothing and runs no Ruby code, so that from the moment it takes
|
|
119
|
+
* those pointers until nghttp3 has copied the Strings, nothing can change,
|
|
120
|
+
* free or move them; so call it after anything that allocates or may run
|
|
121
|
+
* Ruby code, and call nghttp3 right after it. It returns the index of the
|
|
122
|
+
* first element that is not an NV with String name and value and flags of
|
|
123
|
+
* 0..255, or -1 if there is none. nghttp3_rb_raise_bad_nv raises
|
|
124
|
+
* ArgumentError for that index. */
|
|
125
|
+
long nghttp3_rb_nva_fill(VALUE headers, nghttp3_nv *nva, long nvlen);
|
|
126
|
+
NORETURN(void nghttp3_rb_raise_bad_nv(long index));
|
|
92
127
|
|
|
93
128
|
/* Callbacks helper: install the C callbacks, which do their own
|
|
94
129
|
* bookkeeping and then call the Ruby blocks of conn's Callbacks, if any. */
|
|
@@ -513,22 +513,19 @@ static int nghttp3_rb_shutdown_callback(nghttp3_conn *conn, int64_t id,
|
|
|
513
513
|
|
|
514
514
|
typedef struct {
|
|
515
515
|
VALUE proc;
|
|
516
|
-
const
|
|
516
|
+
const nghttp3_proto_settings *settings;
|
|
517
517
|
} settings_call;
|
|
518
518
|
|
|
519
|
+
/* The hash has the settings the peer sent, as nghttp3 parsed them. */
|
|
519
520
|
static VALUE recv_settings_body(VALUE arg) {
|
|
520
521
|
const settings_call *call = (const settings_call *)arg;
|
|
521
|
-
const
|
|
522
|
+
const nghttp3_proto_settings *settings = call->settings;
|
|
522
523
|
|
|
523
|
-
/* Create a Ruby hash with settings values */
|
|
524
524
|
VALUE rb_settings = rb_hash_new();
|
|
525
525
|
rb_hash_aset(rb_settings, ID2SYM(rb_intern("max_field_section_size")),
|
|
526
526
|
ULL2NUM(settings->max_field_section_size));
|
|
527
527
|
rb_hash_aset(rb_settings, ID2SYM(rb_intern("qpack_max_dtable_capacity")),
|
|
528
528
|
SIZET2NUM(settings->qpack_max_dtable_capacity));
|
|
529
|
-
rb_hash_aset(rb_settings,
|
|
530
|
-
ID2SYM(rb_intern("qpack_encoder_max_dtable_capacity")),
|
|
531
|
-
SIZET2NUM(settings->qpack_encoder_max_dtable_capacity));
|
|
532
529
|
rb_hash_aset(rb_settings, ID2SYM(rb_intern("qpack_blocked_streams")),
|
|
533
530
|
SIZET2NUM(settings->qpack_blocked_streams));
|
|
534
531
|
rb_hash_aset(rb_settings, ID2SYM(rb_intern("enable_connect_protocol")),
|
|
@@ -540,9 +537,10 @@ static VALUE recv_settings_body(VALUE arg) {
|
|
|
540
537
|
return Qnil;
|
|
541
538
|
}
|
|
542
539
|
|
|
543
|
-
static int
|
|
544
|
-
|
|
545
|
-
|
|
540
|
+
static int
|
|
541
|
+
nghttp3_rb_recv_settings2_callback(nghttp3_conn *conn,
|
|
542
|
+
const nghttp3_proto_settings *settings,
|
|
543
|
+
void *conn_user_data) {
|
|
546
544
|
ConnectionObj *obj = conn_user_data;
|
|
547
545
|
if (NIL_P(obj->callbacks)) {
|
|
548
546
|
return 0;
|
|
@@ -575,7 +573,7 @@ void nghttp3_rb_setup_callbacks(nghttp3_callbacks *callbacks) {
|
|
|
575
573
|
callbacks->end_stream = nghttp3_rb_end_stream_callback;
|
|
576
574
|
callbacks->reset_stream = nghttp3_rb_reset_stream_callback;
|
|
577
575
|
callbacks->shutdown = nghttp3_rb_shutdown_callback;
|
|
578
|
-
callbacks->
|
|
576
|
+
callbacks->recv_settings2 = nghttp3_rb_recv_settings2_callback;
|
|
579
577
|
}
|
|
580
578
|
|
|
581
579
|
void Init_nghttp3_callbacks(void) {
|