Skip to content

v1.5.0

Latest

Choose a tag to compare

@sjanel sjanel released this 20 Aug 18:28
· 32 commits to main since this release
Immutable release. Only release title and notes can be modified.
73693b3

[1.5.0] - 2026-08-20

Breaking changes

  • HTTP CONNECT tunneling is now disabled by default: an empty HttpServerConfig::connectAllowlist now rejects every
    CONNECT target with 403 Forbidden instead of allowing every resolved host. This closes an unauthenticated SSRF/open
    proxy in default configurations across HTTP/1.1 and HTTP/2. Applications that intentionally provide CONNECT tunnels
    must explicitly configure every trusted target hostname or IP with withConnectAllowlist() (or the
    connectAllowlist JSON/YAML setting). Setting the allowlist to ["*"] deliberately restores the previous unrestricted
    behavior, allowing every host and port including loopback, private-network, link-local, and cloud metadata targets.
  • HttpRequestView::headerValue, headerValueOrEmpty, trailerValue, trailerValueOrEmpty, hasHeader and hasTrailer are now all expecting lower ASCII case keys. This is enforced by the new parameter LowerAsciiKey that will fail to compile for constant strings (for instance, headerValue("Host") does not compile, but headerValue("host") does)
  • HttpRequestView::headers() && trailers() now return a case-sensitive map with lower case keys: header names are normalized to lower-case when parsed, but the returned map have case sensitive look-ups. For instance, the code req.headers().find("X-Header") is now wrong (it can never match) and should be replaced with req.headers().find("x-header"). For simple lookups, prefer above methods that are safer and simpler.
  • http::Connection, http::ContentType, http::Host, etc. are now LowerAsciiKey instead of std::string_view (compile-time validated once at their definition instead of at every call site). Fully backward-compatible for typical usage (implicit conversion back to std::string_view is available) -- only generic/template code asserting the exact type std::string_view would need adjustment. Pure formatting helpers (http::ContentTypeHeaderSep and similar *Sep constants, which are not header-name lookup keys) are unaffected and remain std::string_view.
  • File::Identity becomes private and File::identity() has been replaced with File::appendIdentityData(): File::Identity is now a private nested type, and the public File::identity() method has been removed. The new File::appendIdentityData(char* pData) method writes the file's current descriptor identity and metadata to the provided buffer, returning a pointer past the last written byte. The buffer must be at least File::kIdentitySize bytes long.
  • Global header values should be trimmed of OWS. This is to save some work on the http message finalization path.

Bug Fixes

  • Client: heap-buffer-overflow when adding a header to a body-less request built with reserved header capacity: HttpClient::makeRequest(additionalCapacity, method, url) (the overload without a body) was incorrectly computing indexes to its internal buffer.
  • HTTP/1.1 request is now rejected if it does not contain a Host header: the server now returns 400 Bad Request for HTTP/1.1 requests that do not include a Host header, per RFC 9112 §3.2.
  • Router updates could race server startup: a route update submitted while the server was preparing to run could mutate the router directly while startup clamped route configuration, causing intermittent literal-route assertions. Startup now publishes a synchronized Starting state before launching its thread, so subsequent updates are queued for the event-loop thread.
  • Predicate and stop-token shutdown could leave a listener open without an event loop: when cancellation arrived between event-loop iterations, runUntil() could reset its lifecycle without closing the listener or active connections. New TCP connections then succeeded but were never serviced, most visibly as intermittent 10-second Windows CI stalls. Predicate-driven exits now perform teardown on the event-loop thread, and lifecycle tests use a bounded listener-closure check. Tests: tests/http-server-lifecycle_test.cpp (StartAndStopWhen, StartWithStopToken), tests/multi-http-server_test.cpp (StartDetachedWithStopTokenStopsOnRequest).
  • Async handlers: heap-use-after-free when a connection was closed during a long deferWork(): a request whose async handler ran slow background work (e.g. a multi-second database query) could have its connection swept by the keep-alive idle timeout while the work was still in flight. When the background thread finished it wrote its result into the coroutine frame / connection memory that had already been freed by the event-loop thread, which AddressSanitizer reported as a heap-use-after-free. The fix is threefold: (1) DeferredWorkAwaitable now stores the result/exception in a shared_ptr state co-owned by the background thread and copies the event-loop post-callback at construction, so the worker never dereferences the HttpRequestView or the coroutine frame after completion; (2) connections with an active async handler are excluded from the keep-alive deadline sweep and re-armed once the response is flushed, so an in-flight request is no longer closed as if idle; (3) each connection carries a monotonic generation token that is validated before a posted async completion runs its pre-resume work or resumes the coroutine, so a stale completion can never resume a different connection that reused the same fd. Applies to both HTTP/1.x and HTTP/2 async paths. Tests: aeronet/http/test/http-request-view_test.cpp (DeferredWorkCompletionOutlivesAwaitableStorage), tests/http-routing_test.cpp (DeferredWorkIsNotSweptByKeepAliveTimeout).
  • Fixed HPACK dynamic table desynchronization: entries are now always added to the dynamic table on incremental indexing, even when the resulting header is rejected as malformed, keeping compression state in sync with the peer per RFC 9113 §4.3.
  • HTTP/1.1 now rejects field names that aren't valid tokens (RFC 9110 §5.6.2), and field values containing NUL, bare CR, or bare LF (RFC 9112 §2.2), instead of accepting them silently.
  • HTTP/2 now rejects field names containing uppercase ASCII or invalid bytes, and field values containing NUL/CR/LF, per RFC 9113 §8.2.1, instead of accepting them silently.
  • HTTP/2 now rejects malformed pseudo-header field sections: duplicate, misordered, undefined, response-only, and
    missing request pseudo-headers, plus invalid CONNECT pseudo-header shapes, produce a stream-level
    RST_STREAM(PROTOCOL_ERROR) without closing the connection. Invalid HPACK encoding remains a connection-level
    COMPRESSION_ERROR, and malformed field sections are still decoded in full to keep the HPACK dynamic table in sync.
    Extended CONNECT's defined :protocol field is rejected contextually because its setting is not supported. Unsupported
    extension methods now return 501 Not Implemented instead of being dispatched as GET.
  • HTTP/2 now enforces SETTINGS_MAX_HEADER_LIST_SIZE for decoded header fields: initial and trailing header blocks whose RFC 9113 header-list size exceeds the configured Http2Config::maxHeaderListSize are rejected with RST_STREAM(ENHANCE_YOUR_CALM) before reaching request handling.
  • A handler slower than keepAliveTimeout could get its own response swept away as idle: the keep-alive deadline is armed when the request is read, so a handler that took longer than keepAliveTimeout (a multi-second database query, say) returned with the deadline already expired and the next maintenance sweep closed the connection as if it had been idle - while the response it had just produced was still going out. HTTP/1.1 hands its whole response to the kernel in one go and rarely noticed, but HTTP/2 parks whatever exceeds the peer's flow-control window until a WINDOW_UPDATE arrives: that tail was dropped, and since a downloading HTTP/2 peer keeps sending frames the kernel answered close() with RST rather than FIN, so clients got a successful 200 with fewer bytes than content-length (unexpected EOF) instead of the response the handler returned. keepAliveTimeout bounds idleness between requests (see HttpServerConfig::keepAliveTimeout), so the idle window now restarts when the work completes. Active HTTP/2 streams are excluded from keep-alive reaping until their responses complete, including while waiting for peer flow-control credit; normal idle expiry resumes once no streams remain active. Tests: tests/http2-core_test.cpp (SlowHandlerDoesNotGetItsOwnResponseSweptAsIdle), aeronet/client/test/http-client-http2-e2e_test.cpp (SlowHandlerLargeResponseSurvivesKeepAliveSweep).
  • Fixed macOS EventLoop::add() potentially masking a failed filter registration when adding both EVFILT_READ and EVFILT_WRITE in one call; each filter's result is now checked individually via EV_RECEIPT.

Improvements

  • Faster and smaller router dispatch: literal-route open-addressing slots now store compact side-table indices, and RoutingResult references immutable route metadata instead of copying configuration and middleware ranges. The result shrank from 96 to 48 bytes, the hot Router::match symbol shrank by 39%, and pinned internal benchmarks show about 45% lower median latency for deterministic literal hits.
  • Faster HTTP/1.1 client response capture: completed chunked and decompressed bodies now transfer suitably sized scratch allocations into HttpResponse instead of making a final body copy, while oversized scratch remains reusable for small bodies. The 256 KiB chunked parser microbenchmark is about 39% faster.
  • Faster, stricter HTTP/1.1 client response parsing: chunk-size scanning, hexadecimal decoding, OWS/extension handling, overflow checks, and CRLF consumption now run in one forward pass; status/header/trailer lines use SIMD-accelerated SearchCRLF, and chunk-data delimiters use direct validation. The client now rejects bare-LF framing and consistently requires CRLF, matching the server parser. Chunked-response benchmarks are about 25% faster for 1 KiB chunks and 68% faster for 16-byte chunks.
  • Added a Material for MkDocs documentation site with structured English navigation, local/CI validation, and GitHub Pages deployment alongside the live benchmark dashboards.
  • Replace std::to_chars(int) with faster custom WriteInt
  • **Get rid of <aeronet/stringconv.hpp>: removed internal function StringToTimeISO8601UTC that now becomes useless.
  • Remove limit of number of settings in SETTINGS frame header in HTTP2: The HTTP/2 spec does not limit the number of settings in a SETTINGS frame, but aeronet previously limited it to 16. This limit has been removed, and the SETTINGS frame is now parsed according to the spec without any arbitrary limit.
  • Fix MSVC harmless warnings of unreachable code in ndigits.hpp
  • Fix gcc harmless warnings of sign conversion in ndigits.hpp
  • Optimized HPACK Huffman decoding with a smaller lookup table, canonical fallback, and correct padding handling, significantly improving encode/decode performance. Reworked static header lookup using a collision-free perfect hash and optimized dynamic table insertion/eviction to eliminate unnecessary memory moves. Removed redundant Huffman length computation during encoding and reduced memory usage, yielding up to 46% faster decoding, 30% faster round-trips, and 63% faster static header lookups.
  • Benchmark profiling workflow: scripts/profile_benchmark.sh now supports reliable command/PID recording, existing perf.data post-processing, SVG flamegraphs, and Hotspot AppImage discovery. Scripted server and client benchmarks can profile each measured workload directly with --profile.
  • Simplify and optimize ParseHeaderLine: use std::string_view::find that decays to std::memchr for faster colon search, and added force inline to gain an additional 7 % speedup (and no extra code generation). Total expected speedup are roughly ~40% for typical browser headers, ~30% for API proxy headers, and ~0% for short names headers. See benchmarks/internal/init-try-set-head_bench.cpp for the new benchmark coverage.
  • Faster HTTP/1 CRLF search: SearchCRLF now scans the first 128 bytes with baseline SSE2 on x86 before falling back to libc memchr, improving the realistic request-corpus microbenchmark by about 10%. Non-SSE2 targets retain the portable memchr path. See benchmarks/internal/search-crlf_bench.cpp and aeronet/tech/test/memory-utils_test.cpp.
  • Faster mime mappings lookup: DetermineMIMETypeStr now uses a binary search on extension codes on 64 bits instead of comparing std::string_views to lowercase. Function gains around 40% efficiency on average.
  • Further request head buffer parsing optimizations: faster CI hashing for headers, faster request method parsing. Measured gains of ~10% for requests with very few headers, and up to ~23% for requests with many headers. See benchmarks/internal/init-try-set-head_bench.cpp for the new benchmark coverage.
  • Improved header & trailer lookups by normalizing keys to lower case in HttpRequestView.

Others

  • Bumped glaze version to 8.1.0.