http3 0.0.4 → 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: 327825057f3faad23ce063df236945bb927803d537b9a9df853352ad30515fb4
4
- data.tar.gz: 19051a40291f678fce8a6a44fce125d40706e6ad3cf4b9b800875910dfd5a11b
3
+ metadata.gz: 0ef6daf3337a94169eaba8c25785f154b2c06ed104fefaaf4877c1b162b521cf
4
+ data.tar.gz: 24f9d81b87b20b5ba7f18e0551a2ecdb252eb7ee9bca5220a421c1a57cc6f652
5
5
  SHA512:
6
- metadata.gz: 6003280f0c3dbcd4d62d4a63f7c70a79b31230c93dcc2ad7d95bfd83b3ae98a9814fa7e076e1ccb6c8e476b8b8822a94a7daefffbab7d0db19752752c5b3a929
7
- data.tar.gz: b10b118decf20b8326cabd055890d35a7c0b8e17aa1853e61e726761f679b98a35c0cc178a1250fd43e1311a616e1b32b47cbf58e3ff239bd87f55cc1d0ac24a
6
+ metadata.gz: 4d13a1693067a9e03f0d197423d21b10ff9115ebc584a2c848db43e3bda958d6382120ba78251f62bed5d71932b081c3a9e66ba7f3c404a602b0d69fbc2f7781
7
+ data.tar.gz: a6318fbe24ec6d23c630c811c6ac42a010996dac9db39e630d6db1a61b14bca79661558660dffd190aed875a312aa73654fe02fa905395834280ee54b866df4a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,49 @@
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
+
1
47
  ## [0.0.4] - 2026-10-27
2
48
 
3
49
  ### 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,5 +1,8 @@
1
1
  # http3-ruby
2
2
 
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)
5
+
3
6
  `http3` serves Rack 3 applications over HTTP/3. It runs QUIC through the
4
7
  [quic gem](https://github.com/unasuke/quic-ruby) and HTTP/3 through
5
8
  [nghttp3](https://github.com/ngtcp2/nghttp3), which it builds from source and
@@ -8,11 +11,21 @@ links in statically. The nghttp3 bindings are also there to use directly, as
8
11
 
9
12
  It is not suited for production yet:
10
13
 
11
- - The application is called on the thread that drives every connection, one
12
- request at a time, so a slow request holds up all the others.
13
- - A request body is read in full, up to `max_request_body:` (1 MiB), before
14
- the application is called, and streaming response bodies are not
15
- supported.
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.
16
29
  - CI runs it on Ruby 4.0 alone, not on 3.3, the oldest it installs on. It is
17
30
  checked against curl and the clients of quic-interop-runner, over links
18
31
  with no loss or delay, and has not been run under load or over lossy
@@ -62,40 +75,86 @@ Other options, with their defaults:
62
75
  | Option | Default | |
63
76
  | --- | --- | --- |
64
77
  | `max_connections:` | `1024` | Connections at once; more are refused with CONNECTION_REFUSED |
78
+ | `threads:` | `3` | Worker threads that call the application |
65
79
  | `max_request_body:` | `1_048_576` | Bytes of request body; more gets 413 |
66
80
  | `max_field_section_size:` | `65_536` | Bytes of request headers (RFC 9114 §4.2.2); more gets 431 |
67
- | `max_connection_request_buffer:` | `4_194_304` | Bytes a connection's requests may hold before they reach the application (headers counted as RFC 9114 §4.2.2 does, and bodies); more gets 503 |
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 |
68
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 |
69
88
  | `send_buffer_size:` | `65_536` | Bytes of a response body queued but unsent per stream |
70
- | `max_connection_send_buffer:` | `1_048_576` | Bytes of array and `each` response bodies a connection may have queued but unsent; more waits |
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 |
71
90
  | `max_total_send_buffer:` | `67_108_864` | The same over all connections |
72
- | `request_timeout:` | `30` | Seconds a request may take to come in full, counted from its headers; later gets 408. `nil` for no limit |
73
- | `refusal_log_interval:` | `60` | Seconds between the `requests_refused` log lines, which count the requests answered without the application (400, 408, 413, 431, 501, 503) |
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:` |
74
94
  | `shutdown_timeout:` | `30` | Seconds a graceful stop waits for the requests under way |
75
95
  | `qlog_dir:` | `nil` | Directory for one qlog file per connection |
76
96
  | `transport_params:` | `QUIC::TransportParams.default` | QUIC transport parameters |
77
97
  | `settings:` | `QUIC::Settings.default` | QUIC settings (the ALPN is always `h3`) |
78
98
  | `logger:` | `Logger.new($stderr)` | Where failures are logged |
79
99
 
80
- `max_connection_request_buffer:` has to be at least `max_request_body:` plus
81
- `max_field_section_size:`, and `max_total_request_buffer:` at least
82
- `max_connection_request_buffer:`, so that a request within the limits never
83
- gets 503 from an idle server; otherwise `HTTP3::Server.new` raises
84
- `ArgumentError`.
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.
85
133
 
86
134
  A request gets 413, 431 or 503 as soon as it passes a limit, without waiting
87
135
  for the rest of it, and the server reads no more of it (STOP_SENDING with
88
136
  H3_NO_ERROR). The 503 has neither headers nor body.
89
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
+
90
142
  A request gets 400, without the application seeing it, when its scheme is
91
143
  not `http` or `https`, its path has a fragment, its authority is not `host`
92
144
  or `host:port`, or its `:authority` and `host` field differ. `CONNECT` gets
93
145
  501: the server opens no tunnels.
94
146
 
95
147
  Response bodies may be files (`to_path`, sent 16 KiB at a time as the
96
- connection takes them), arrays or anything with `each`. These get 500:
97
- streaming bodies (`call`), which are not supported; a status outside
98
- 200..599; response headers whose names are not lowercase tokens or whose
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
99
158
  values contain NUL, CR or LF; a `content-length` given more than once, not a
100
159
  number, or not the length of a file or array body; and an array body with a
101
160
  chunk that is not a String. A body read with `each` is counted as it is
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
@@ -193,7 +193,7 @@ static const quic_ext_api *http3_load_quic_api(uint32_t major) {
193
193
  VALUE version = rb_const_get(quic, rb_intern("VERSION"));
194
194
  rb_raise(rb_eLoadError,
195
195
  "http3 needs the quic gem's C function table for quic/ext.h "
196
- "ABI major version %u (quic ~> 0.0.5), which quic %" PRIsVALUE
196
+ "ABI major version %u (quic ~> 0.0.6), which quic %" PRIsVALUE
197
197
  " does not provide",
198
198
  (unsigned int)major, version);
199
199
  }
data/ext/http3/nghttp3.h CHANGED
@@ -102,7 +102,9 @@ void nghttp3_rb_conn_stream_closed(ConnectionObj *obj, int64_t stream_id);
102
102
 
103
103
  /* Helper functions */
104
104
  VALUE nghttp3_rb_error_class_for_code(int error_code);
105
- 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));
106
108
 
107
109
  /* rb_id (to_int is called) as a stream id for nghttp3, which asserts the
108
110
  * range of the ids it is given: ArgumentError unless it is 0..2**62-1. */
@@ -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) {
@@ -790,7 +790,7 @@ static VALUE rb_nghttp3_connection_stream_writable_p(VALUE self,
790
790
 
791
791
  entered = obj->entered;
792
792
  obj->entered = CONN_IN_OTHER;
793
- rv = nghttp3_conn_is_stream_writable(obj->conn, stream_id);
793
+ rv = nghttp3_conn_is_stream_writable2(obj->conn, stream_id);
794
794
  obj->entered = entered;
795
795
 
796
796
  return rv ? Qtrue : Qfalse;
@@ -248,7 +248,7 @@ static VALUE rb_nghttp3_qpack_encoder_get_num_blocked_streams(VALUE self) {
248
248
  rb_raise(rb_eNghttp3InvalidStateError, "Encoder is not initialized");
249
249
  }
250
250
 
251
- num = nghttp3_qpack_encoder_get_num_blocked_streams(obj->encoder);
251
+ num = nghttp3_qpack_encoder_get_num_blocked_streams2(obj->encoder);
252
252
 
253
253
  return SIZET2NUM(num);
254
254
  }
@@ -554,7 +554,7 @@ static VALUE rb_nghttp3_qpack_decoder_decoder_stream_data(VALUE self) {
554
554
  rb_raise(rb_eNghttp3InvalidStateError, "Decoder is not initialized");
555
555
  }
556
556
 
557
- len = nghttp3_qpack_decoder_get_decoder_streamlen(obj->decoder);
557
+ len = nghttp3_qpack_decoder_get_decoder_streamlen2(obj->decoder);
558
558
 
559
559
  if (len == 0) {
560
560
  return rb_str_new("", 0);