What's Changed
New features: WebSocket read timeout
ws::WebSocket::read()now reports a read timeout you set yourself as the newReadResult::Timeout, and leaves the connection open. Previously every failure, a timeout included, returnedFailand marked the connection closed, so a read timeout could not be used to get control back and send on the same connection.Timeoutis reported only when the timeout elapses on a message boundary with nothing consumed; a timeout partway through a fragmented message is stillFail.msgis left untouched onTimeout. Only a timeout set at runtime throughset_read_timeout()comes back this way; the compile-time defaults are a backstop against a peer that has gone quiet, and when one of them elapsesread()returnsFailand closes the connection as before, so code that never callsset_read_timeout()can keep usingwhile (ws.read(msg))- Because
Timeoutis non-zero,while (ws.read(msg))is not usable once you have set a read timeout: the loop body would run again with the previous message still inmsg. Test the result instead:ws.set_read_timeout(std::chrono::milliseconds(100)); std::string msg; while (ws.is_open()) { auto r = ws.read(msg); if (r == httplib::ws::Timeout) { continue; } // nothing yet; send if you like if (r == httplib::ws::Fail) { break; } handle(msg); }
- Add
ws::WebSocket::set_read_timeout()(seconds/microseconds andstd::chronooverloads), so a server handler can bound its own reads, for example to relay between connections instead of parking inread().WebSocketClient::set_read_timeout()now also takes effect on an already-open connection instead of only seeding the nextconnect() CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECONDis split intoCPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND(default0, wait forever) andCPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND(default300). A client's read timeout is the caller's tool for taking back control, not a liveness check (that is ping/pong's job, seeset_websocket_max_missed_pongs()), so aWebSocketClientno longer gives up on its own after 300s of silence. A server keeps the 300s ceiling that reclaims a worker from a peer that has gone quiet. The old macro still works, sets both, and emits a deprecation#pragma message
Security-relevant fixes
- Send each credential only to the hop that reads it (#2579). An
SSLClientbehind a proxy sentProxy-Authorizationinside the TLS tunnel, where the origin reads it, and sent the origin'sAuthorizationfromset_basic_auth()/set_bearer_token_auth()on theCONNECTrequest, where the proxy reads it in plaintext before the tunnel exists. Headers fromset_default_headers(), commonly origin credentials such asAuthorization,Cookieor API keys, were attached toCONNECTas well. Each now goes only on the message its own hop reads - Reject ambiguously framed responses on the client (#2581). A response carrying both
Transfer-Encodingand a non-zeroContent-Length(RFC 9112 §6.3) is now rejected withError::Readby both the buffered read path andopen_stream(). The client delimited such a body by the transfer coding while an intermediary may go byContent-Length, so the two could disagree on where the body ends and desynchronize a reused connection. The server already rejected the same framing on requests; the check now lives in one shared helper. HEAD, 204 and 304 responses are exempt,Content-Length: 0is tolerated, and a response whose final transfer coding is not chunked is still read until the connection closes, as RFC 9112 specifies for responses
Bug fixes
- Reject a
Rangefirst-byte-pos that overflowsssize_twith 416 (#2580). Whenparse_range_headermoved ontodetail::from_chars, an out-of-range first-byte-pos stopped being an error and left the "no first-byte-pos" sentinel in place, sobytes=9223372036854775808-100was served as the suffix rangebytes=-100. An oversized last-byte-pos is still accepted, since RFC 9110 §14.1.2 reads it as "the remainder of the representation" - Don't compress a response whose handler already set
Content-Encoding(#2575). A handler serving content it encoded itself, such as an asset compressed at build time, now keeps its own coding and bytes. This covers file-backed responses too: with static file compression enabled, a mount point orset_file_content()response that named its coding had its stored bytes compressed a second time and got a secondContent-Encodingfield line.Vary: Accept-Encodingis added only to a coding the server chose, so a handler that picks a representation fromAccept-Encodingshould setVaryitself - Read every multi-byte WebSocket frame field until it is complete.
Stream::read()may return fewer bytes than requested, but only the payload was read in a loop, so a frame header that straddled the read buffer's boundary failed the frame and closed the connection - Record an error reason on the two
WebSocketSSLStream::read()failure paths that returned -1 without one, soget_error()can no longer report a previous call's timeout
Documentation
- README: document that a response already carrying
Content-Encodingis sent as is, and when a handler has to addVaryitself - README-websocket and cookbook W01/W02/W04/W06 (en/ja): cover
ReadResult::Timeout, which timeout produces it, the split client/server read timeout macros, and thewhile (ws.read(msg))pitfall once a read timeout is set
Development
- test/Makefile: put
$(EXTRA_CXXFLAGS)after-fsanitize=addressso it can override it (#2577) - CI: style-check now runs on macOS with Homebrew's clang-format, and the pre-commit hook calls the system clang-format, so local commits and CI use the same binary
- Proxy tests: replace the external httpbin-style sites (httpcan.org, httpbingo.org) with a self-hosted go-httpbin container behind nginx in the squid docker-compose stack, so an upstream outage no longer fails CI
- CI: remove the temporary windows-without-SSL flaky failure reporter now that the failures it tracked have stopped
Version 0.55.0 was tagged locally but never published; this release supersedes it.
Full Changelog: v0.54.1...v0.56.0