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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7342e51b5150f53092806631b85d5d1053d16f7f757c26730227fd0a9d27619d
4
- data.tar.gz: 7c7cee88475696117fcfe14e42333fc1267ffc0942c40360f6b6d0fcc9b487cf
3
+ metadata.gz: 0ef6daf3337a94169eaba8c25785f154b2c06ed104fefaaf4877c1b162b521cf
4
+ data.tar.gz: 24f9d81b87b20b5ba7f18e0551a2ecdb252eb7ee9bca5220a421c1a57cc6f652
5
5
  SHA512:
6
- metadata.gz: 802c51e3f9040ac9d51ff0f6afee6e1541f7f479f94336283d49c346c127bf6740fca313ce4badb5627ba03e4793be9cd1536bafbfec24a180a3658a2a0c3cd7
7
- data.tar.gz: fe45066ac7c54c622ff9c5fdab9d1fbc474867ce2076db6fd28f1c78ca4f65db8c5fbf1059b2dead1234e414902f8d43aa53c9912a08602fd0849425a5bfc480
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
@@ -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.13.1 -- https://github.com/ngtcp2/nghttp3
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.13.1, lib/sfparse) -- https://github.com/ngtcp2/sfparse
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
- > [!CAUTION]
4
- > This gem is for experimental purposes only. Do not use it in production environments.
5
-
6
- > [!WARNING]
7
- > The code in this repository is almost entirely generated by Claude Code and reviewed by a human.
3
+ [![CI](https://github.com/unasuke/http3-ruby/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/unasuke/http3-ruby/actions/workflows/main.yml)
4
+ [![interop](https://github.com/unasuke/http3-ruby/actions/workflows/interop.yml/badge.svg?branch=main)](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 or anything with `each`. Streaming bodies
70
- (`call`) are not supported and get 500, as do response headers whose names
71
- are not lowercase tokens or whose values contain NUL, CR or LF.
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
- # nghttp3_conn_read_stream2 needs 1.12.0 or later. nghttp3 depends on nothing
9
- # but libc, so its version does not have to follow the quic gem's ngtcp2.
10
- NGHTTP3_VERSION = "1.13.1"
11
- NGHTTP3_SHA256 = "07160f28af3ddc3e8b95c8bbefe08c650e7cf303375141b6ca35cc89b319f70d"
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.3 or later provides it)" unless have_header("quic/ext.h")
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.3), which quic %" PRIsVALUE
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 helper */
91
- nghttp3_nv nghttp3_rb_nv_to_c(VALUE rb_nv);
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 nghttp3_settings *settings;
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 nghttp3_settings *settings = call->settings;
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 nghttp3_rb_recv_settings_callback(nghttp3_conn *conn,
544
- const nghttp3_settings *settings,
545
- void *conn_user_data) {
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->recv_settings = nghttp3_rb_recv_settings_callback;
576
+ callbacks->recv_settings2 = nghttp3_rb_recv_settings2_callback;
579
577
  }
580
578
 
581
579
  void Init_nghttp3_callbacks(void) {