feat(tcp): socket errors as std::system_error, with the phase that failed
SocketErrorException is now a std::system_error: code() carries the errno
under std::generic_category(), or getaddrinfo's result under the new
ResolverCategory(), whose message() is gai_strerror's text; `phase` says
which step failed (resolve, open, connect, send, receive). It is still a
std::runtime_error with the message ClientTCP always threw, the code's
text after it, so existing handlers keep working.
Before, the exception carried a `kind` and an int, and a caller that had
to print the cause its own way still had to match the message prefix to
learn where the failure happened. The standard type ends both: the
category knows how to print the number, and the phase is a field. A
caller writes "[Errno {}] {}" from code().value() and code().message(),
or compares code() with std::errc::connection_refused.
Breaking: SocketErrorException's `kind` and `code` members are gone;
read code().category() and code().value().
This commit is contained in:
parent
16da0a1979
commit
e86c3b70a0
9 changed files with 107 additions and 63 deletions
|
|
@ -41,7 +41,7 @@ A route map is `std::unordered_map<std::string, std::function<HTTPResponse(const
|
|||
|
||||
### HTTP/1.1
|
||||
|
||||
**`ClientHTTP1(host, port)`** is plaintext, **`ClientHTTP1(host, port, TLSClientCredentials)`** is `https://`. `Send(request)` returns the `HTTPResponse` on the calling thread, no pool; `SendAsync(request, onSuccess, onError)` runs it as a pool task with the error as a string. The connection is persistent: the first `Send` dials, later ones reuse the socket until the peer asks to close; a pooled connection the peer closed meanwhile is redialled once and the request replayed, never once a response byte has arrived and never on a fresh connection, so a broken server is an exception, not a loop. An empty `authority` means `host:port`, with the scheme's default port left off (443 under TLS, 80 without); `scheme` is ignored (origin-form targets; the constructor decided the transport). Set `limits` (`HTTP1::MessageLimits`: start line 8 KiB, header section 64 KiB, body 512 MiB), `timeout` (30 s for the next piece of a response) and `handshakeTimeout` (15 s) before the first `Send`. `Connected()`, `Secure()`, `Protocol()` (the ALPN the last connection agreed, `"http/1.1"` under TLS), `Disconnect()`. One instance serves one caller at a time; instances are independent. Throws `HTTP1::HTTP1ProtocolError` for a response it refuses to parse, `TLSException` for a certificate it rejects, `std::runtime_error` for a socket that cannot connect, times out or closes early. There is no connect timeout: the dial is a `ClientTCP` with the default zero, so a host that never answers blocks `Send` for the kernel's two minutes before `handshakeTimeout` and `timeout` even start.
|
||||
**`ClientHTTP1(host, port)`** is plaintext, **`ClientHTTP1(host, port, TLSClientCredentials)`** is `https://`. `Send(request)` returns the `HTTPResponse` on the calling thread, no pool; `SendAsync(request, onSuccess, onError)` runs it as a pool task with the error as a string. The connection is persistent: the first `Send` dials, later ones reuse the socket until the peer asks to close; a pooled connection the peer closed meanwhile is redialled once and the request replayed, never once a response byte has arrived and never on a fresh connection, so a broken server is an exception, not a loop. An empty `authority` means `host:port`, with the scheme's default port left off (443 under TLS, 80 without); `scheme` is ignored (origin-form targets; the constructor decided the transport). Set `limits` (`HTTP1::MessageLimits`: start line 8 KiB, header section 64 KiB, body 512 MiB), `connectTimeout` (15 s, per address the host resolves to), `handshakeTimeout` (15 s) and `timeout` (30 s for the next piece of a response) before the first `Send`. `Connected()`, `Secure()`, `Protocol()` (the ALPN the last connection agreed, `"http/1.1"` under TLS), `Disconnect()`. One instance serves one caller at a time; instances are independent. Throws `HTTP1::HTTP1ProtocolError` for a response it refuses to parse, `TLSException` for a certificate it rejects, `SocketErrorException` for a socket that cannot resolve or connect or breaks under the request, `SocketTimeoutException` when the connect runs out, and `std::runtime_error` when a response stops arriving or the connection closes before one is complete.
|
||||
|
||||
**`ListenerHTTP1(port, routes[, fallback][, TLSServerCredentials])`** is a `ListenerTCP` underneath (every IPv4 address, `SO_REUSEADDR`) and throws `std::runtime_error` when the port cannot be bound, or `TLSException` when the credentials yield no usable certificate. `Listen()` runs the accept loop on the calling thread until `Stop()`, which stops accepting, closes every live connection and joins their threads. **`ListenerAsyncHTTP1`**, same constructors, runs that loop on its `thread` and exposes the `listener`; `Stop()` joins and the destructor stops. Each accepted connection gets its own thread, served one request at a time until the peer closes, sends `connection: close`, or a timeout passes: `keepAliveTimeout` 15 s between requests, `requestTimeout` 30 s between pieces of one request (each read, not the whole request), `handshakeTimeout` 15 s for TLS; `limits` as on the client. `ConnectionCount()`, `AcceptedCount()`, `HandshakeFailureCount()` (connections dropped in the TLS handshake: a scanner, a client with no protocol in common, an untrusted client certificate; ordinary on a public port), `Secure()`.
|
||||
|
||||
|
|
@ -63,12 +63,12 @@ A route map is `std::unordered_map<std::string, std::function<HTTPResponse(const
|
|||
|
||||
### TCP
|
||||
|
||||
**`ClientTCP(host, port, connectTimeout = 0ms)`** resolves with `getaddrinfo`, IPv4 only, and connects; zero waits out the kernel's retries (about two minutes to a host that never answers), a timeout throws `SocketTimeoutException` after that long, and a refused connect throws `std::runtime_error` at once. `ClientTCP(int socket)` adopts a descriptor. Move-only; the destructor shuts down and closes `socketid`, a public member. `SetNoDelay(true)` for small frames that must leave at once; `SetReceiveTimeout(ms)` makes a receive that waits longer throw `SocketTimeoutException`, losing what a whole-buffer receive had collected; `Stop()` closes; `Connect(timeout)` after it opens a new socket to the host and port the client was built from (not on one that adopted a descriptor, which recorded no address).
|
||||
**`ClientTCP(host, port, connectTimeout = 0ms)`** resolves with `getaddrinfo` for IPv4 and IPv6 and tries every address in the order it returned them, so `localhost` reaches a server on `::1` alone; zero waits out the kernel's retries per address (about two minutes to a host that never answers), a timeout throws `SocketTimeoutException` after that long per address, and when no address connects what the last one failed with is thrown. A failed resolve, `socket()`, connect, send or receive throws `SocketErrorException`, a `std::system_error`: `code()` is the errno under `std::generic_category()` or the `getaddrinfo` result under `Crafter::ResolverCategory()` (whose `message()` is `gai_strerror`'s), and `phase` is the step (`SocketPhase::Resolve`, `Open`, `Connect`, `Send`, `Receive`); `what()` is the message ClientTCP always threw with the code's text after it, so a handler for `std::runtime_error` still catches it, and `[Errno {}] {}` is `code().value()` and `code().message()`. `ClientTCP(int socket)` adopts a descriptor. Move-only; the destructor shuts down and closes `socketid`, a public member. `SetNoDelay(true)` for small frames that must leave at once; `SetReceiveTimeout(ms)` makes a receive that waits longer throw `SocketTimeoutException`, losing what a whole-buffer receive had collected; `Stop()` closes; `Connect(timeout)` after it opens a new socket to the host and port the client was built from (not on one that adopted a descriptor, which recorded no address).
|
||||
|
||||
- `Send(buffer, size)` writes it all. The receives return what arrived, at least one byte: `RecieveSync()` one chunk, `RecieveSync(n)` up to `n`, `RecieveUntilFullSync(n)` exactly `n`, `RecieveUntilCloseSync()` everything to the peer's close, `RecieveSync(n, buffer)` into your memory, returning the count. Asked for zero bytes they return at once. They throw `SocketClosedException` once the peer has closed, `SocketTimeoutException` on a receive timeout, `std::runtime_error` otherwise, and wait on through a signal.
|
||||
- `Send(buffer, size)` writes it all. The receives return what arrived, at least one byte: `RecieveSync()` one chunk, `RecieveSync(n)` up to `n`, `RecieveUntilFullSync(n)` exactly `n`, `RecieveUntilCloseSync()` everything to the peer's close, `RecieveSync(n, buffer)` into your memory, returning the count. Asked for zero bytes they return at once. They throw `SocketClosedException` once the peer has closed, `SocketTimeoutException` on a receive timeout, `SocketErrorException` otherwise, and wait on through a signal.
|
||||
- The `*Async` receives (`RecieveAsync`, `RecieveUntilCloseAsync`, `RecieveAsync(n, ...)`, `RecieveUntilFullAsync(n, ...)`, and `RecieveAsync(n, callback(count), buffer, ...)` into memory that must outlive the call) run the same receive as a pool task and take an `onError(std::exception_ptr)`: what the receive throws goes there, and an empty `onError` drops it, since an exception leaving a pool task ends the program. The task uses the `ClientTCP`, which must outlive it; a parked receive ends through `Stop()` from another thread or a receive timeout, both of which arrive at `onError`.
|
||||
|
||||
**`ListenerTCP(port, connectCallback, concurrentClientLimit, totalClientLimit)`** binds every IPv4 address with `SO_REUSEADDR`, backlog 128, and throws `std::runtime_error` when it cannot bind or listen. The callback gets a heap-allocated `ClientTCP*` and owns it: delete it when the connection is done. `ListenSyncSync()` accepts on the calling thread and calls the callback there; `ListenSyncAsync()` calls it as a pool task; `ListenAsyncSync()` and `ListenAsyncAsync()` run the accept loop itself as a pool task, the second with the callback as another. `Stop()` closes the socket, which ends the loop. `totalClientLimit` ends it after that many accepts; `concurrentClientLimit` is stored and not enforced.
|
||||
**`ListenerTCP(port, connectCallback, concurrentClientLimit, totalClientLimit)`** binds every IPv4 address with `SO_REUSEADDR`, backlog 128, and throws `std::runtime_error` when it cannot bind or listen; **`ListenerTCP(address, port, ...)`** binds that one address, IPv4 or IPv6, and port 0 lets the kernel pick, reported by `Port()`. The callback gets a heap-allocated `ClientTCP*` and owns it: delete it when the connection is done. `ListenSyncSync()` accepts on the calling thread and calls the callback there; `ListenSyncAsync()` calls it as a pool task; `ListenAsyncSync()` and `ListenAsyncAsync()` run the accept loop itself as a pool task, the second with the callback as another. `Stop()` closes the socket, which ends the loop. `totalClientLimit` ends it after that many accepts; `concurrentClientLimit` is stored and not enforced.
|
||||
|
||||
### QUIC
|
||||
|
||||
|
|
@ -107,14 +107,14 @@ On a `wasm32-*` target the same `ClientHTTP` and `ClientQUIC` compile against th
|
|||
|
||||
| Call | On failure |
|
||||
|---|---|
|
||||
| `ClientTCP` constructor, `Connect`, `Send`, `Recieve*Sync` | `std::runtime_error` (resolve, refused, socket errors); `SocketTimeoutException` (connect or receive timeout); `SocketClosedException` (the peer closed) |
|
||||
| `ClientTCP` constructor, `Connect`, `Send`, `Recieve*Sync` | `SocketErrorException` (a `std::system_error`: a failed resolve, `socket()`, connect, send or receive, with `phase` and `code()`); `SocketTimeoutException` (connect or receive timeout); `SocketClosedException` (the peer closed) |
|
||||
| `ClientTCP::Recieve*Async` | the same, as an `std::exception_ptr` to `onError`; dropped without one |
|
||||
| `ListenerTCP`, `ListenerHTTP1`, `ListenerAsyncHTTP1` constructors | `std::runtime_error` when the port cannot be bound; `TLSException` for unusable credentials |
|
||||
| `ListenerHTTP`, `ListenerAsyncHTTP` constructors | `QUICException` (they build a `ListenerQUIC`) |
|
||||
| `ClientHTTP1(host, port, TLSClientCredentials)` constructor | `TLSException` for an unreadable certificate or trust anchor, at construction rather than at the first `Send` |
|
||||
| `TLSContext::Server`/`Client`, `TLSStream::Connect`/`Accept` | `TLSException`, naming the certificate error |
|
||||
| `ByteStream::ReadSome` / `Write` | `std::runtime_error`; a read that times out is `StreamStatus::TimedOut`, a write that does throws |
|
||||
| `ClientHTTP1::Send` | `HTTP1::HTTP1ProtocolError`, `TLSException`, `std::runtime_error` (`timed out waiting for the response`, `connection closed before a complete response arrived`, the socket's) |
|
||||
| `ClientHTTP1::Send` | `HTTP1::HTTP1ProtocolError`, `TLSException`, `SocketErrorException` (the socket's), `SocketTimeoutException` (the connect), `std::runtime_error` (`timed out waiting for the response`, `connection closed before a complete response arrived`) |
|
||||
| `ClientHTTP1::SendAsync`, `ClientHTTP::SendAsync` | `what()` as a string to `onError` |
|
||||
| `HTTP1::MessageParser`, `SerializeRequest`/`SerializeResponse` | `HTTP1::HTTP1ProtocolError` |
|
||||
| `ClientQUIC` constructor, `OpenStream`, `SendDatagram`, `GetStreamId`, `ListenerQUIC` constructor | `QUICException` (native; the browser build's `ClientQUIC` never throws, see "Browser build") |
|
||||
|
|
@ -123,7 +123,7 @@ On a `wasm32-*` target the same `ClientHTTP` and `ClientQUIC` compile against th
|
|||
| `ClientHTTP::Send` | `QUICException`, `QUICClosedException`, `HTTP3::HTTP3ProtocolError` |
|
||||
| Route handlers and fallbacks | an exception becomes a 500; a WT handler's exception is swallowed |
|
||||
|
||||
`SocketClosedException`, `SocketTimeoutException` and `QUICClosedException` derive from `std::exception`, not `std::runtime_error`; `QUICException`, `TLSException`, `HTTP1ProtocolError` and `HTTP3ProtocolError` do, so a `catch (const std::runtime_error&)` misses the first three.
|
||||
`SocketClosedException`, `SocketTimeoutException` and `QUICClosedException` derive from `std::exception`, not `std::runtime_error`; `SocketErrorException` (through `std::system_error`), `QUICException`, `TLSException`, `HTTP1ProtocolError` and `HTTP3ProtocolError` do, so a `catch (const std::runtime_error&)` misses the first three.
|
||||
|
||||
### Spellings
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue