{"id":31363831,"url":"https://github.com/sjanel/aeronet","last_synced_at":"2026-01-18T02:55:42.785Z","repository":{"id":315899318,"uuid":"1060350953","full_name":"sjanel/aeronet","owner":"sjanel","description":"A modern C++ HTTP/1.1 server for Linux","archived":false,"fork":false,"pushed_at":"2025-09-21T12:46:44.000Z","size":1604,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-09-21T14:38:15.223Z","etag":null,"topics":["cpp","cpp23","http-server","linux"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sjanel.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":"docs/ROADMAP.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-19T19:06:31.000Z","updated_at":"2025-09-21T12:46:47.000Z","dependencies_parsed_at":"2025-09-21T14:38:20.264Z","dependency_job_id":"ed3dc841-90ea-46ba-839d-564690f6f834","html_url":"https://github.com/sjanel/aeronet","commit_stats":null,"previous_names":["sjanel/aeronet"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/sjanel/aeronet","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sjanel%2Faeronet","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sjanel%2Faeronet/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sjanel%2Faeronet/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sjanel%2Faeronet/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sjanel","download_url":"https://codeload.github.com/sjanel/aeronet/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sjanel%2Faeronet/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":277184144,"owners_count":25775286,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-27T02:00:08.978Z","response_time":73,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cpp","cpp23","http-server","linux"],"created_at":"2025-09-27T05:22:57.973Z","updated_at":"2026-01-18T02:55:42.766Z","avatar_url":"https://github.com/sjanel.png","language":"C++","funding_links":[],"categories":["Web Application Framework"],"sub_categories":[],"readme":"# aeronet\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"resources/logo-blue-violet.png\" alt=\"aeronet\" width=\"360\" /\u003e\n\u003c/p\u003e\n\n[![CI](https://github.com/sjanel/aeronet/actions/workflows/ci.yml/badge.svg)](https://github.com/sjanel/aeronet/actions/workflows/ci.yml)\n[![Coverage](https://codecov.io/gh/sjanel/aeronet/branch/main/graph/badge.svg)](https://codecov.io/gh/sjanel/aeronet)\n[![Packaging](https://github.com/sjanel/aeronet/actions/workflows/packaging.yml/badge.svg)](https://github.com/sjanel/aeronet/actions/workflows/packaging.yml)\n[![clang-format](https://github.com/sjanel/aeronet/actions/workflows/clang-format-check.yml/badge.svg)](https://github.com/sjanel/aeronet/actions/workflows/clang-format-check.yml)\n[![Benchmarks](https://img.shields.io/endpoint?url=https%3A%2F%2Fsjanel.github.io%2Faeronet%2Fbenchmarks%2Fbenchmark_badge.json)](https://sjanel.github.io/aeronet/benchmarks/)\n[![Release](https://img.shields.io/github/v/release/sjanel/aeronet?style=flat-square)](https://github.com/sjanel/aeronet/releases/latest)\n\n## Why aeronet?\n\n**aeronet** is a modern, fast, modular and ergonomic HTTP / WebSocket C++ **server library** for **Linux** focused on predictable performance, explicit control and minimal dependencies.\n\n- **Fast \u0026 predictable**: edge‑triggered reactor model, zero/low‑allocation hot paths and minimal copies, horizontal scaling with port reuse. In CI benchmarks `aeronet` ranks among the [fastest tested implementations](#performance-at-a-glance) across multiple realistic scenarios.\n- **Modular \u0026 opt‑in**: enable only the features you need at compile time to minimize binary size and dependencies\n- **Ergonomic**: easy API, automatic features (encoding, telemetry), RAII listener setup with sync / async server lifetime control, developer friendly with no hidden global state, no macros\n- **Configurable**: extensive dynamic configuration with reasonable defaults (principle of least surprise), per path options and middleware helpers, run-time router / config updates\n- **Standards compliant**: HTTP/1.1, HTTP/2, WebSocket, Compression, Streaming, Trailers, TLS, CORS, Range \u0026 Conditional Requests, Static files, URL Decoding, multipart/form-data, etc.\n- **Cloud native**: Built-in Kubernetes-style health probes, opentelemetry support (metrics, tracing) with built-in spans and metrics, dogstatsd support, perfect for micro-services\n\n### Performance at a glance\n\n`aeronet` is designed to be **very fast**. In our automated [wrk](https://github.com/wg/wrk)-based benchmarks (HTTP/1.1 based) against other popular frameworks (run in CI against a fixed set of competitors such as [drogon](https://github.com/drogonframework/drogon), [pistache](https://github.com/pistacheio/pistache), a Rust Axum server, Java Undertow, Go and Python), `aeronet`:\n\n- Achieves the **highest requests/sec** in most scenarios\n- Consistently delivers **lower average latency** in those same scenarios\n- Maintains **competitive or better throughput and memory usage**\n\nYou can inspect the latest benchmark tables generated on `main` from the CI **benchmarks** job and detailed methodology here:\n\n- [Latest CI benchmarks (CI workflow, benchmarks job)](https://github.com/sjanel/aeronet/actions/workflows/ci.yml?query=branch%3Amain)\n- [Benchmark scenarios and methodology](benchmarks/scripted-servers/README.md)\n\nYou can browse the latest rendered benchmark tables directly on GitHub Pages:\n\n- [Live benchmark dashboard](https://sjanel.github.io/aeronet/benchmarks/)\n\n## Minimal Examples\n\nSpin up a basic HTTP server that responds on `/hello` in just a few lines.\n**All code examples** in the `README` and the `FEATURES.md` files are guaranteed to compile as they are covered by a CI check.\n\n### Immediate response\n\nReturn a complete, immediate `HttpResponse` from the handler:\n\n```cpp\n#include \u003caeronet/aeronet.hpp\u003e // unique 'umbrella' header, includes all public API\n\nusing namespace aeronet;\n\nint main() {\n  Router router;\n  router.setPath(http::Method::GET, \"/hello\", [](const HttpRequest\u0026 req) {\n    return HttpResponse(200).header(\"X-Req-Body\", req.body()).body(\"hello from aeronet\\n\");\n  });\n  HttpServer server(HttpServerConfig{}, std::move(router)); // default port is ephemeral, OS will pick an available one\n  server.run(); // blocking. Use start() for non-blocking\n}\n```\n\nSee the [full program](examples/minimal.cpp).\n\n### Streaming response\n\nFor a large, unknown size response body, reply with multiple body chunks using `HttpResponseWriter`, that will use HTTP chunked transfer encoding automatically:\n\n```cpp\nRouter router;\nrouter.setDefault([](const HttpRequest\u0026 req, HttpResponseWriter\u0026 writer){\n  writer.status(200);\n  writer.header(\"X-Req-Path\", req.path());\n  writer.contentType(\"text/plain\");\n  for (int i = 0; i \u003c 10; ++i) {\n    writer.writeBody(std::string(50,'x')); // write by chunks\n  }\n  writer.end();\n});\n```\n\n### Async handler (Coroutines)\n\nFor a large request body or an asynchronous operation that may take a long time, use an async handler returning `RequestTask\u003cHttpResponse\u003e`:\n\n```cpp\n// Minimal awaitable used for the README demo so `co_await someAsyncOperation()` compiles.\nstruct SomeAsyncAwaitable {\n  bool await_ready() const noexcept { return false; }\n  void await_suspend(std::coroutine_handle\u003c\u003e h) noexcept { h.resume(); }\n  std::string await_resume() const noexcept { return std::string(\"Hello from coroutine!\"); }\n};\n\nSomeAsyncAwaitable someAsyncOperation() { return {}; }\n\nint main() {\n  Router router;\n  router.setPath(http::Method::GET, \"/async\", [](HttpRequest\u0026 req) -\u003e RequestTask\u003cHttpResponse\u003e {\n    // Suspend execution without blocking the thread\n    auto result = co_await someAsyncOperation();\n    co_return HttpResponse(200).body(result);\n  });\n}\n```\n\nAsync handlers are invoked as soon as the request head is parsed, even if the body is still streaming in.\nCall `co_await req.bodyAwaitable()` (or the chunked helpers) before touching the body to wait for the buffered payload.\n\nYou can refer to the [complete async handlers example](examples/async-handlers.cpp) for more details.\n\n### HTTP/2 support\n\n`aeronet` is compatible with HTTP/2, with or without TLS, when built with `-DAERONET_ENABLE_HTTP2=ON`.\n\nWhen `AERONET_ENABLE_HTTP2` is OFF, the HTTP/2 module is not built and the HTTP/2-specific API surface (e.g. `Http2Config`, `HttpServerConfig::withHttp2()`) is not available.\n\nHTTP/2 uses the same unified `HttpRequest` type as HTTP/1.1:\n\n```cpp\n#include \u003caeronet/aeronet.hpp\u003e\n\nusing namespace aeronet;\n\nint main() {\n  Router router;\n  \n  // Single handler works for both HTTP/1.1 and HTTP/2\n  router.setDefault([](const HttpRequest\u0026 req) {\n    if (req.isHttp2()) {\n      return HttpResponse{\"Hello from HTTP/2! Stream: \" + std::to_string(req.streamId()) + \"\\n\"};\n    }\n    return HttpResponse{\"Hello from HTTP/1.1\\n\"};\n  });\n\n  HttpServerConfig config;\n  config.withPort(8443)\n      .withTlsCertKey(\"server.crt\", \"server.key\")\n      .withTlsAlpnProtocols({\"h2\", \"http/1.1\"})\n      .withHttp2(Http2Config{.enable = true});\n\n  SingleHttpServer server(std::move(config), std::move(router));\n  server.run();\n}\n```\n\nTest: `curl -k --http2 https://localhost:8443/hello`\n\nSee the [full HTTP/2 example](examples/http2.cpp) for more details.\n\n## Quick Start with provided examples\n\nMinimal server examples for typical use cases are provided in [examples](examples) directory.\n\n```bash\ncmake -S . -B build -DCMAKE_BUILD_TYPE=Release\ncmake --build build -j\n\n./build/examples/aeronet-minimal 8080   # or omit 8080 for ephemeral\n```\n\nTest with curl:\n\n```bash\ncurl -i http://localhost:8080/hello\n\nHTTP/1.1 200\ncontent-type: text/plain\nserver: aeronet\ndate: Sun, 04 Jan 2026 15:49:40 GMT\ncontent-length: 151\n\nHello from aeronet minimal server! You requested /hello\n...\n```\n\n## Detailed Documentation\n\nThe following focused docs expand each area without cluttering the high‑level overview:\n\n- [Feature reference (FEATURES)](docs/FEATURES.md)\n\n- [WebSocket](docs/FEATURES.md#websocket-rfc-6455)\n- [HTTP/2](docs/FEATURES.md#http2-rfc-9113)\n- [Compression \u0026 Negotiation](docs/FEATURES.md#compression--negotiation)\n- [Static File Handler \u0026 Range Requests](docs/FEATURES.md#static-file-handler-rfc-7233--rfc-7232)\n- [Inbound Request Decompression](docs/FEATURES.md#inbound-request-decompression-config-details)\n- [Multipart/form-data utilities](docs/FEATURES.md#multipartform-data-utilities-rfc-7578)\n- [Connection Close Semantics](docs/FEATURES.md#connection-close-semantics)\n- [Reserved \u0026 Managed Headers](docs/FEATURES.md#reserved--managed-response-headers)\n- [Query String \u0026 Parameter Decoding](docs/FEATURES.md#query-string--parameters)\n- [Trailing Slash Policy](docs/FEATURES.md#trailing-slash-policy)\n- [Routing patterns \u0026 path parameters](docs/FEATURES.md#routing-patterns--path-parameters)\n- [HttpServer Lifecycle](docs/FEATURES.md#multihttpserver--lifecycle)\n- [TLS Features](docs/FEATURES.md#tls-features)\n\nIf you are evaluating the library, the feature highlights above plus the minimal example are usually sufficient. Dive into the docs only when you need specifics.\n\n## Feature Matrix (Concise)\n\n| Category | Implemented (✔) | Notes |\n|----------|-----------------|-------|\n| Core HTTP/1.1 parsing | ✔ | Request line, headers, chunked bodies, pipelining |\n| Routing | ✔ | Exact path + method allow‑lists; streaming + fixed |\n| Keep‑Alive / Limits | ✔ | Header/body size, max requests per connection, idle timeout |\n| Compression (gzip/deflate/zstd/br) | ✔ | Flags opt‑in; q‑value negotiation; threshold; per‑response opt‑out |\n| Inbound body decompression | ✔ | Multi‑layer, safety guards, header removal |\n| TLS | ✔ (flag) | ALPN, mTLS, session tickets, kTLS sendfile, timeouts, metrics |\n| OpenTelemetry | ✔ (flag) | Distributed tracing spans, metrics counters (experimental) |\n| Async wrapper | ✔ | Background thread convenience |\n| Metrics hook | ✔ (alpha) | Per‑request basic stats |\n| Logging | ✔ (flag) | spdlog optional |\n| Duplicate header policy | ✔ | Deterministic, security‑minded |\n| WebSocket | ✔ | RFC 6455 compliant, text/binary frames, ping/pong, close handshake |\n| HTTP/2 | ✔ (flag) | RFC 9113, HPACK, ALPN h2, h2c upgrade, stream multiplexing |\n| Trailers exposure | ✔ | RFC 7230 §4.1.2 chunked trailer headers |\n| Middleware helpers | ✔ | Global + per-route request/response hooks (streaming-aware) |\n| Streaming inbound decompression | ✔ | Auto-switches to streaming inflaters once Content-Length exceeds configured threshold |\n| sendfile / static file helper | ✔ | 0.4.x – zero-copy plain sockets plus RFC 7233 single-range \u0026 RFC 7232 validators |\n\n## Developer / Operational Features\n\n| Feature | Notes |\n|---------|-------|\n| Epoll edge-triggered loop | One thread per `SingleHttpServer`; writev used for header+body scatter-gather |\n| `SO_REUSEPORT` scaling | Horizontal multi-reactor capability |\n| Multi-instance wrapper | `MultiHttpServer` orchestrates N reactors (N threads) |\n| Async server methods | `start()` (void convenience) and `startDetached()` (returns `AsyncHandle`) |\n| Move semantics | Transfer listening socket \u0026 loop state safely |\n| Restarts | `SingleHttpServer` and `MultiHttpServer` can be started again after stop |\n| Graceful draining | `SingleHttpServer::beginDrain(maxWait)` stops new accepts, closes keep-alive after current responses, optional deadline to force-close stragglers |\n| Signal handling | Optional built-in SIGINT/SIGTERM handler to initiate draining when stop requested |\n| Heterogeneous lookups | Path handler map accepts `std::string`, `std::string_view`, `const char*` |\n| Outbound stats | Bytes queued, immediate vs flush writes, high-water marks |\n| Lightweight logging | Pluggable design (spdlog optional); ISO 8601 UTC timestamps |\n| Builder-style config | Fluent `HttpServerConfig` setters (`withPort()`, etc.) |\n| Metrics callback | Per-request timing \u0026 size scaffold hook |\n| RAII construction | Fully listening after constructor (ephemeral port resolved immediately) |\n| Comprehensive tests | Parsing, limits, streaming, mixed precedence, reuseport, move semantics, keep-alive |\n| Mixed handlers example | Normal + streaming coexistence on same path (e.g. GET streaming, POST fixed) |\n\nThe sections below provide a more granular feature matrix and usage examples.\n\n## HTTP/1.1 Feature Matrix\n\nMoved out of the landing page to keep things concise. See the full, continually updated matrices in:\n\n- [HTTP/1.1 Feature Matrix](docs/FEATURES.md#http11-feature-matrix)\n- [Performance / architecture](docs/FEATURES.md#performance--architecture)\n\n## Public objects and usage\n\nConsuming `aeronet` will result in the client code interacting with [server objects](#server-objects), [router](#router-configuration-two-safe-ways-to-set-handlers), [http responses](#building-the-http-response), streaming HTTP responses and reading HTTP requests.\n\n### Server objects\n\n`aeronet` provides 2 types of servers: `SingleHttpServer` and `MultiHttpServer`.\nClient code will mostly use `MultiHttpServer` because it's the one supporting multi-threaded scaling out of the box, but `SingleHttpServer` is also available for simpler use cases or when the user wants to manage multiple server instances manually.\nFor convenience, a `HttpServer` alias is provided for `MultiHttpServer` which is the recommended default server type.\nThese are the main objects expected to be used by the client code.\n\n#### SingleHttpServer\n\nThe core server of `aeronet`. It is a mono-threaded process based on a reactor pattern powered by `epoll` with a blocking running event loop.\nThe call to `run()` (or `runUntil(\u003cpredicate\u003e)`) is blocking, and can be stopped by another thread by calling `stop()` on this instance.\nThe non-blocking APIs launch the event loop in the background. Use `start()` when you want a void convenience that manages an internal handle for you, or `startDetached()` (and the related `startDetachedAndStopWhen(\u003cpredicate\u003e)`, `startDetachedWithStopToken(\u003cstop token\u003e)`) when you need an `AsyncHandle` you can inspect or control explicitly.\n\nKey characteristics:\n\n- It is a **RAII** class - and actually `aeronet` library as a whole does not have any singleton for a cleaner \u0026 expected design (except for signal handlers, but it's because signals themselves are global), so all resources linked to the `SingleHttpServer` are tied (and will be released with) it.\n- It is **copyable** and **moveable** if and only if it is **not running**.\n**Warning!** Unlike most C++ objects, the move operations are not `noexcept` to make sure that client does not move a running server (it would throw in that case, and only in that case). Moving a non-running `SingleHttpServer` is, however, perfectly safe and `noexcept` in practice.\n- It is **restartable**, you can call `start()` after a `stop()`.\n- You can modify most of its **configuration safely at runtime** via `postConfigUpdate()` and `postRouterUpdate()`.\n- Graceful draining is available via `beginDrain(std::chrono::milliseconds maxWait = 0)`: it stops accepting new connections, lets in-flight responses finish with `Connection: close`, and optionally enforces a deadline before forcing the remaining connections to close.\n\n##### Configuration\n\nAll configuration of the `SingleHttpServer` is applied per **server instance** (the server **owns** its configuration).\n\n`SingleHttpServer` takes a `HttpServerConfig` by value at construction, which allows full control over the server parameters (port, timeouts, limits, TLS setup, compression options, etc). Once constructed, some fields can be updated, even while the server is running thanks to `postConfigUpdate` method.\n\nNote that `nbThreads` field should be 1 for `SingleHttpServer`. If you intend to use multiple threads, consider using `HttpServer` (aka `MultiHttpServer`) instead.\n\n#### Running an asynchronous event loop (non blocking)\n\nA convenient set of methods on a `SingleHttpServer` that allow non blocking:\n\n`start()` — non-blocking convenience (returns void); the server manages an internal handle.\n\n`startDetached()` — non-blocking; returns an `AsyncHandle` giving explicit lifecycle control.\n\n`startDetachedAndStopWhen(\u003cpredicate\u003e)` — like `startDetached()` but stops when predicate fires.\n\n`startDetachedWithStopToken(\u003cstop token\u003e)` — like `startDetached()` but integrates with `std::stop_token`.\n\nThese methods allow running the server in a background thread; pick `startDetached()` when you need the handle, or `start()` when you do not.\n\n```cpp\n#include \u003caeronet/aeronet.hpp\u003e\nusing namespace aeronet;\n\nint main() {\n  Router router;\n  router.setDefault([](const HttpRequest\u0026){ return HttpResponse(200).body(\"hi\"); });\n  SingleHttpServer srv(HttpServerConfig{}, std::move(router));\n  // Launch in background thread and capture lifetime handle\n  auto handle = srv.startDetached();\n  // main thread free to do orchestration / other work\n  std::this_thread::sleep_for(std::chrono::seconds(2));\n  handle.stop();\n  handle.rethrowIfError();\n}\n```\n\nPredicate form (stop when external flag flips):\n\n```cpp\nstd::atomic\u003cbool\u003e done{false};\nSingleHttpServer srv(HttpServerConfig{});\nauto handle = srv.startDetachedAndStopWhen([\u0026]{ return done.load(); });\n// later\ndone = true; // loop exits soon (bounded by poll interval)\n```\n\nStop-token form (std::stop_token):\n\n```cpp\n// If you already manage a std::stop_source you can pass its token directly\n// to let the caller control the server lifetime via cooperative cancellation.\nstd::stop_source src;\nSingleHttpServer srv(HttpServerConfig{});\nauto handle = srv.startDetachedWithStopToken(src.get_token());\n// later\nsrc.request_stop();\n```\n\nNotes:\n\n- Register handlers before `start()` unless you provide external synchronization for modifications.\n- `stop()` is idempotent; destructor performs it automatically as a safety net.\n- keep returned `AsyncHandle` to keep the server running; server will be stopped at its destruction.\n\n#### HttpServer, aka MultiHttpServer, a multi threading version of SingleHttpServer\n\nInstead of manually creating N threads and N `SingleHttpServer` instances, you can use `HttpServer` to spin up a \"farm\" of identical servers with same routing configuration, on the same port. It:\n\n- Accepts a base `HttpServerConfig` (set `port=0` for ephemeral bind; the same chosen port is propagated to all instances)\n- Replicates either a global handler or all registered path handlers across each underlying server (even after in-flight updates)\n- Exposes `stats()` returning both per-instance and aggregated totals (sums; `maxConnectionOutboundBuffer` is a max)\n- Provides the resolved listening `port()` directly after construction (even for ephemeral port 0 requests)\n- Provides the same lifecycle APIs as `SingleHttpServer`: blocking `run()` / `runUntil(pred)`, non-blocking `start()` / `startDetached()`, `stop()`, `beginDrain()`, etc.\n- Like `SingleHttpServer`, `HttpServer` is copyable and moveable when not running, and restartable after stop.\n\nExample:\n\n```cpp\n#include \u003caeronet/aeronet.hpp\u003e\nusing namespace aeronet;\n\nint main() {\n  Router router;\n  router.setDefault([](const HttpRequest\u0026 req){\n    return HttpResponse(200).body(\"hello\\n\");\n  });\n  HttpServer multi(HttpServerConfig{}.withNbThreads(4), std::move(router)); // 4 underlying event loops\n  multi.start();\n  // ... run until external signal, or call stop() ...\n  std::this_thread::sleep_for(std::chrono::seconds(30));\n  auto agg = multi.stats();\n  log::info(\"instances={} queued={}\\n\", agg.per.size(), agg.total.totalBytesQueued);\n}\n```\n\nAdditional notes:\n\n- If `cfg.port` was 0 the kernel-chosen ephemeral port printed above will remain stable across any later `stop()` /\n  `start()` cycles for this `HttpServer` instance. To obtain a new ephemeral port you must construct a new `HttpServer` (or in a future API explicitly reset the base configuration before a restart to `port=0`).\n- You may call `stop()` and then `start()` again on the same `HttpServer` instance.\n- Handlers: global or path handlers registered are re-applied to the fresh servers on each\n  restart. You may add/remove/replace path handlers using `postRouterUpdate()` or `router()` at any time (even during running).\n- Per‑run statistics are not accumulated across restarts; each run begins with fresh counters (servers rebuilt).\n\nStats aggregation example:\n\n```cpp\nHttpServer multi(HttpServerConfig{}.withNbThreads(4), Router{});\nauto st = multi.stats();\nfor (size_t i = 0; i \u003c st.per.size(); ++i) {\n  const auto\u0026 s = st.per[i];\n  log::info(\"[srv{}] queued={} imm={} flush={}\\n\", i,\n             s.totalBytesQueued,\n             s.totalBytesWrittenImmediate,\n             s.totalBytesWrittenFlush);\n}\n```\n\n##### Example\n\n```bash\n./build/examples/aeronet-multi 8080 4   # port 8080, 4 threads\n```\n\nEach thread owns its own listening socket (`SO_REUSEPORT`) and epoll instance – no shared locks in the accept path.\nThis is the simplest horizontal scaling strategy before introducing a worker pool.\n\n#### Summary table\n\n| Variant | Header | Launch API | Blocking? | Threads Created | Scaling Model | Typical Use Case | Restartable? | Notes |\n|---------|--------|------------|-----------|--------------------|---------------|------------------|--------------|-------|\n| `SingleHttpServer` | `aeronet/single-http-server.hpp` | `run()` / `runUntil(pred)` | Yes (caller thread blocks) | 0 | Single reactor | Dedicated thread you manage or simple main-thread server | Yes | Minimal overhead, zero thread creation |\n| `SingleHttpServer` | `aeronet/single-http-server.hpp` | `start()` (void convenience) / `startDetached()` / `startDetachedAndStopWhen(pred)` / `startDetachedWithStopToken(token)` | No (`startDetached()` returns `AsyncHandle`) | 1 `std::jthread` (owned by handle) | Single reactor (background) | Non-blocking single server, calling thread remains free | Yes | `startDetached()` returns RAII handle; `start()` is a void convenience |\n| `HttpServer` | `aeronet/http-server.hpp` | `run()` / `runUntil(pred)` | Yes (caller thread blocks) | N (`threadCount`) | Horizontal `SO_REUSEPORT` multi-reactor | Multi-core throughput, blocking orchestration | Yes | All reactors run on caller thread until stop |\n| `HttpServer` | `aeronet/http-server.hpp` | `start()` (void convenience) / `startDetached()` | No (`startDetached()` returns `AsyncHandle`) | N `std::jthread`s (internal) | Horizontal `SO_REUSEPORT` multi-reactor | Multi-core throughput, non-blocking launch | Yes | `startDetached()` returns RAII handle; `start()` is a void convenience |\n\nDecision heuristics:\n\n- Use `SingleHttpServer::run()` / `runUntil()` when you already own a thread (or can block `main()`) and want minimal abstraction with zero overhead.\n- Use `SingleHttpServer::start()` family when you want a single server running in the background while keeping the calling thread free (e.g., integrating into a service hosting multiple subsystems, or writing higher-level control logic while serving traffic). The returned `AsyncHandle` provides RAII lifetime management with no added weight to `SingleHttpServer` itself.\n- Use `HttpServer` when you need multi-core throughput with separate event loops per core – the simplest horizontal scaling path before introducing more advanced worker models.\n\nBlocking semantics summary:\n\n- `SingleHttpServer::run()` / `runUntil()` – fully blocking; returns only on `stop()` or when predicate is satisfied.\n- `SingleHttpServer::start()` / `startDetachedAndStopWhen()` / `startDetachedWithStopToken()` – non-blocking; returns immediately with an `AsyncHandle`. Lifetime controlled via the handle's destructor (RAII) or explicit `handle.stop()`.\n- `MultiHttpServer::run()` / `runUntil()` – fully blocking; returns only on `stop()` or when predicate is satisfied.\n- `MultiHttpServer::start()` – non-blocking; returns after all reactors are launched, manages internal thread pool.\n\n#### Signal-driven Shutdown (Process-wide)\n\n`aeronet` provides a global signal handler mechanism for graceful shutdown of **all** running servers:\n\n```cpp\n// Install signal handlers for SIGINT/SIGTERM (typically in main before starting servers)\nstd::chrono::milliseconds maxDrainPeriod{5000}; // 5s max drain\nSignalHandler::Enable(maxDrainPeriod);\n\n// All SingleHttpServer instances regularly check for stop requests in their event loops\nSingleHttpServer server(HttpServerConfig{});\nserver.run();  // Will drain and stop when SIGINT/SIGTERM received\n```\n\nKey points:\n\n- **Process-wide**: `SignalHandler::Enable()` installs handlers that set a global flag checked by all `SingleHttpServer` instances (and so, `HttpServer` instances are also affected).\n- **Automatic drain**: When a signal arrives, all running servers automatically call `beginDrain(maxDrainPeriod)` at the next event loop iteration.\n- **Optional**: Don't call `SignalHandler::Enable()` if your application manages signals differently.\n\n### Router configuration: two safe ways to set handlers\n\nRouting configuration may be applied in two different ways depending on your application's lifecycle and threading model. Prefer pre-start configuration when possible; use the runtime proxy when you must mutate routing after server construction.\n\n#### Pre-start configuration (recommended)\n\nConstruct and fully configure a `Router` instance on the calling thread, then pass it to the server constructor. This is the simplest and safest approach: router will be up to date immediately directly at server construction.\n\nExample (recommended):\n\n```cpp\nRouter router;\nrouter.setPath(http::Method::GET, \"/hello\", [](const HttpRequest\u0026){ return HttpResponse(200).body(\"hello\"); });\nSingleHttpServer server(HttpServerConfig{}, std::move(router));\nserver.run();\n```\n\n#### Runtime updates via `RouterUpdateProxy`\n\nIf you need to mutate routes while a server is active, use the `RouterUpdateProxy` exposed by `SingleHttpServer::router()` and by convenience  `HttpServer::router()`. The proxy accepts handler registration calls and forwards them to the server's event-loop thread so updates occur without racing the request processing. If the server is running, the update will be effective at most after one event polling period.\n\nExample (runtime-safe):\n\n```cpp\nSingleHttpServer server(HttpServerConfig{});\nauto handle = server.startDetached();\n// later, from another thread:\nserver.router().setPath(http::Method::POST, \"/upload\", [](const HttpRequest\u0026){ return HttpResponse(201); });\n```\n\nNotes:\n\n- The proxy methods schedule updates to run on the server thread; they may execute immediately when the server is idle, or be queued and applied at the next loop iteration.\n- The proxy will propagate exceptions thrown by your updater back to the caller when possible; handler registration conflicts (e.g. streaming vs non-streaming for same method+path) are reported.\n- Prefer pre-start configuration for simpler semantics and testability; use runtime updates only when dynamic reconfiguration is required.\n\n### Building the HTTP response\n\nThe router expects callback functions returning a `HttpResponse`.\n\nYou have two ways to construct a `HttpResponse`:\n\n- Direct construction thanks to its numerous constructors taking status **code**, **body** \u0026 `content-type`, **headers**, additional capacity for headers/body/trailers\n- [Optimized](#optimize-httpresponse-construction) construction from `HttpRequest::makeResponse()` that pre-applies server-global headers and other optimizations\n\nYou can build it thanks to the numerous provided methods to store the main components of a HTTP response (status code, reason, headers, body and trailers):\n\n| Operation          | Complexity           | Notes                                  |\n|--------------------|----------------------|----------------------------------------|\n| `status()`         | O(1)                 | Overwrites 3 digits                    |\n| `reason()`         | O(trailing)          | One tail `memmove` if size delta       |\n| `header()`         | O(headers + bodyLen) | Linear scan + maybe one shift          |\n| `headerAddLine()`      | O(bodyLen)           | Shift tail once; no scan               |\n| `body()` (inline)  | O(delta) + realloc   | Exponential growth strategy            |\n| `body()` (capture) | O(1)                 | Zero copy client buffer capture        |\n| `bodyStatic()` (capture) | O(1)                 | Zero copy client buffer capture        |\n| `bodyAppend()` (inline) | O(delta) + realloc   | Exponential growth strategy, zero-copy support            |\n| `bodyInlineAppend()` | O(delta) + realloc   | Exponential growth strategy            |\n| `bodyInlineSet()` | O(1) + realloc   | Exact growth strategy            |\n| `file()`           | O(1)                 | Zero-copy sendfile helper              |\n| `trailerAddLine()`     | O(1)                 | Append-only; no scan (only after body) |\n\nUsage guidelines:\n\n- Use `headerAddLine()` when duplicates are acceptable or not possible from the client code (cheapest path).\n- Use `header()` only when you must guarantee uniqueness. Matching is case‑insensitive; prefer a canonical style (e.g.\n  `Content-Type`) for readability, but behavior is the same regardless of input casing.\n- Chain on temporaries for concise construction; the rvalue-qualified overloads keep the object movable.\n- For maximum performance, fill the response in order, starting with status/reason, then headers, then body and trailers, to minimize memory shifts and reallocations.\n\n#### Optimize HttpResponse construction\n\nYou can use `HttpRequest::makeResponse()` methods to optimize some job usually made at finalization time, directly at construction time.\nThis is especially useful when you have configured `globalHeaders` in the server config that you want to apply to all responses, as it avoids copying them again before the body (that would also shift the whole body, if inlined) at response finalization time.\n\nExample:\n\n```cpp\nRouter router;\nrouter.setDefault([](const HttpRequest\u0026 req) {\n  // Pre-applies global headers from server config\n  return req.makeResponse(\"hello\\n\"); // response already contains global headers (for instance: 'server: aeronet')\n});\n```\n\nOverloads make it possible to pass status and / or body \u0026 content-type, very useful for one-shot responses.\n\n#### Reserved Headers\n\nThe library intentionally reserves a small set of response headers that user code cannot set directly on\n`HttpResponse` (fixed responses) or via `HttpResponseWriter` (streaming) because aeronet itself manages them or\ntheir semantics would be invalid / ambiguous without deeper protocol features:\n\nReserved now (assert if attempted in debug; ignored in release for streaming):\n\n- `date` – generated once per second and injected automatically.\n- `content-length` – computed from the body (fixed) or set through `contentLength()` (streaming). Prevents\n  inconsistencies between declared and actual size.\n- `connection` – determined by keep-alive policy (HTTP version, server config, request count, errors). User code\n  supplying conflicting values could desynchronize connection reuse logic.\n- `transfer-encoding` – controlled by streaming writer (`chunked`) or omitted when `content-length` is known. Allowing\n  arbitrary values risks illegal CL + TE combinations or unsupported encodings.\n- `trailer`, `te`, `upgrade` – not yet supported by aeronet; reserving them now avoids future backward-incompatible\n  behavior changes when trailer / upgrade features are introduced.\n\nAllowed convenience helpers:\n\n- `content-type` via `contentType()` in streaming.\n- `location` via `location()` for redirects.\n\n##### Content-Type resolution for static files\n\nWhen serving files with the built-in static helpers, aeronet chooses the response `content-type` using the\nfollowing precedence: (1) user-provided resolver callback if installed and non-empty, (2) the configured default\ncontent type in `HttpServerConfig`, and (3) `application/octet-stream` as a final fallback. The `File::detectedContentType()`\nhelper is available for filename-extension based detection (the built-in mapping now includes common C/C++ extensions\nsuch as `c`, `h`, `cpp`, `hpp`, `cc`).\n\nAll other headers (custom application / caching / CORS / etc.) may be freely set; they are forwarded verbatim.\nThis central rule lives in a single helper (`http::IsReservedResponseHeader`).\n\n## Miscellaneous features\n\n### Connection Close Semantics (CloseMode)\n\nFull details (modes, triggers, helpers) have been moved out of the landing page:\nSee: [Connection Close Semantics](docs/FEATURES.md#connection-close-semantics)\n\n### Compression (gzip, deflate, zstd, brotli)\n\n`aeronet` has built-in support for automatic outbound response compression (and inbound requests decompression) with multiple algorithms, provided that the library is built with each available encoder compile time flag.\n\nDetailed negotiation rules, thresholds, opt-outs, and tuning have moved:\nSee: [Compression \u0026 Negotiation](docs/FEATURES.md#compression--negotiation)\n\nPer-response manual override: setting any `Content-Encoding` (even `identity`) disables automatic compression for that\nresponse. Details \u0026 examples: [Manual Content-Encoding Override](docs/FEATURES.md#per-response-manual-content-encoding-automatic-compression-suppression)\n\n### Inbound Request Body Decompression\n\nDetailed multi-layer decoding behavior, safety limits, examples, and configuration moved here:\nSee: [Inbound Request Decompression](docs/FEATURES.md#inbound-request-decompression-config-details)\n\n### CORS (Cross-Origin Resource Sharing)\n\nFull RFC-compliant CORS support with per-route and router-wide configuration:\nSee: [CORS Support](docs/FEATURES.md#cors-support)\n\n### Request Header Duplicate Handling\n\nDetailed policy \u0026 implementation moved to: [Request Header Duplicate Handling](docs/FEATURES.md#request-header-duplicate-handling-detailed)\n\n### Kubernetes style probes\n\nEnable the builtin probes via `HttpServerConfig` and test them with curl. This example enables the probes with default paths and a plain-text content type.\n\n```cpp\n#include \u003caeronet/aeronet.hpp\u003e\n\nusing namespace aeronet;\n\nint main() {\n  HttpServerConfig cfg;\n  cfg.withBuiltinProbes(BuiltinProbesConfig{});\n\n  Router router;\n  // Register application handlers as usual (optional)\n  router.setPath(http::Method::GET, \"/hello\", [](const HttpRequest\u0026){\n    return HttpResponse(200).body(\"hello\\n\");\n  });\n\n  SingleHttpServer server(std::move(cfg), std::move(router));\n\n  server.run();\n}\n```\n\nProbe checks (from the host/container):\n\n```bash\ncurl -i http://localhost:8080/livez   # expects HTTP/1.1 200 when running\ncurl -i http://localhost:8080/readyz  # expects 200 when ready, 503 during drain/startup\ncurl -i http://localhost:8080/startupz # returns 503 until initialization completes\n```\n\nFor a Kubernetes `Deployment` example that configures liveness/readiness/startup probes against these paths, see: [docs/kubernetes-probes.md](docs/kubernetes-probes.md).\n\n### Zero copy / Sendfile\n\nThere is a small example demonstrating `file` in `examples/aeronet-sendfile`.\nIt exposes two endpoints:\n\n- `GET /static` — returns the contents of a file using `HttpResponse::file` (fixed response).\n- `GET /stream` — returns the contents of a file using `HttpResponseWriter::file` (streaming writer API).\n\nBuild the examples and run the sendfile example:\n\n```bash\ncmake -S . -B build -DCMAKE_BUILD_TYPE=Release\ncmake --build build -j\n./build/examples/aeronet-sendfile 8080 /path/to/file\n```\n\nIf the file path argument is omitted the example creates a small temp file in `/tmp` and serves it.\n\nFetch the file with curl:\n\n```bash\ncurl -i http://localhost:8080/static\ncurl -i http://localhost:8080/stream\n```\n\nThe example demonstrates both the fixed-response (server synthesizes a `content-length` header) and the\nstreaming writer path. For plaintext sockets the server uses the kernel `sendfile(2)` syscall for zero-copy\ntransmission. When TLS is enabled the example exercises the TLS fallback that pread()s into the connection buffer\nand writes through the TLS transport.\n\n## Test Coverage Matrix\n\nSummary of current automated test coverage (see `tests/` directory). Legend: ✅ covered by explicit test(s), ⚠ partial / indirect, ❌ not yet.\n\n| Area | Feature | Test Status | Notes / Representative Test Files |\n|------|---------|-------------|-----------------------------------|\n| Parsing | Request line (method/target/version) | ✅ | `http-parser-errors_test.cpp`, `http-core_test.cpp` |\n| Parsing | Unsupported HTTP version (505) | ✅ | `http-parser-errors_test.cpp` |\n| Parsing | Header parsing \u0026 lookup | ✅ | `http-core_test.cpp`, `http-core_test.cpp` |\n| Limits | Max header size -\u003e 431 | ✅ | `http-core_test.cpp` |\n| Limits | Max body size (Content-Length) -\u003e 413 | ✅ | `http-additional_test.cpp` |\n| Limits | Chunk total/body growth -\u003e 413 | ✅ | exercised across `http-chunked-head_test.cpp` and parser fuzz paths |\n| Bodies | Content-Length body handling | ✅ | `http-core_test.cpp`, `http-additional_test.cpp` |\n| Bodies | Chunked decoding | ✅ | `http-chunked-head_test.cpp`, `http-parser-errors_test.cpp` |\n| Bodies | Trailers exposure | ✅ | Implemented (see tests/http-trailers_test.cpp) |\n| Expect | 100-continue w/ non-zero length | ✅ | `http-parser-errors_test.cpp` |\n| Expect | No 100 for zero-length | ✅ | `http-parser-errors_test.cpp`, `http-additional_test.cpp` |\n| Keep-Alive | Basic keep-alive persistence | ✅ | `http-core_test.cpp` |\n| Keep-Alive | Max requests per connection | ✅ | `http-additional_test.cpp`|\n| Keep-Alive | Idle timeout close | ⚠ | Indirectly covered; explicit idle-time tests are planned |\n| Pipelining | Sequential pipeline of requests | ✅ | `http-additional_test.cpp` |\n| Pipelining | Malformed second request handling | ✅ | `http-additional_test.cpp` |\n| Methods | HEAD semantics (no body) | ✅ | `http-chunked-head_test.cpp`, `http-additional_test.cpp` |\n| Date | RFC7231 format + correctness | ✅ | `http-core_test.cpp` |\n| Date | Same-second caching invariance | ✅ | `http-core_test.cpp` |\n| Date | Second-boundary refresh | ✅ | `http-core_test.cpp` |\n| Errors | 400 Bad Request (malformed line) | ✅ | `http-core_test.cpp` |\n| Parsing | Percent-decoding of path | ✅ | `http-url-decoding_test.cpp`, `http-query-parsing_test.cpp` |\n| Errors | 431, 413, 505, 501 | ✅ | `http-core_test.cpp`, `http-additional_test.cpp` |\n| Errors | PayloadTooLarge in chunk decoding | ⚠ | Exercised indirectly; dedicated test planned |\n| Concurrency | `SO_REUSEPORT` distribution | ✅ | `multi-http-server_test.cpp` |\n| Lifecycle | Move semantics of server | ✅ | `http-server-lifecycle_test.cpp` |\n| Lifecycle | Graceful stop (runUntil) | ✅ | many tests use runUntil patterns |\n| Diagnostics | Parser error callback (version, bad line, limits) | ✅ | `http-parser-errors_test.cpp` |\n| Diagnostics | PayloadTooLarge callback (Content-Length) | ⚠ | Indirect; explicit capture test planned |\n| Performance | Date caching buffer size correctness | ✅ | covered by `http-core_test.cpp` assertions |\n| Performance | writev header+body path | ⚠ | Indirectly exercised; no direct assertion yet |\n| TLS | Handshake \u0026 rejection behavior | ✅ | `http-tls-handshake_test.cpp`, `http-tls-io_test.cpp` |\n| Streaming | Streaming response \u0026 incremental flush | ✅ | `http-streaming_test.cpp` |\n| Routing | Path \u0026 method matching | ✅ | `http-routing_test.cpp`, `router_test.cpp` |\n| Compression | Negotiation \u0026 outbound insertion | ✅ | `http-compression_test.cpp`, `http-request-decompression_test.cpp` |\n| OpenTelemetry | Basic integration smoke | ✅ | `opentelemetry-integration_test.cpp` |\n| Async run | SingleHttpServer::start() behavior | ✅ | `http-server-lifecycle_test.cpp` |\n| Misc / Smoke | Probes, stats, misc invariants | ✅ | `http-server-lifecycle_test.cpp`, `http-stats_test.cpp` |\n| Implemented | Trailers (outgoing chunked / trailing headers) | ✅ | See tests/http-trailers_test.cpp and http-response-writer.hpp |\n\n## Build \u0026 Installation\n\nFull, continually updated build, install, and package manager instructions live in [`docs/INSTALL.md`](docs/INSTALL.md).\n\nQuick start (release build of examples):\n\n```bash\ncmake -S . -B build -DCMAKE_BUILD_TYPE=Release\ncmake --build build -j\n```\n\nFor TLS toggles, sanitizers, Conan/vcpkg usage and `find_package` examples, see the INSTALL guide.\n\n## Trailing Slash Policy\n\nFull resolution algorithm and matrix moved to: [Trailing Slash Policy](docs/FEATURES.md#trailing-slash-policy)\n\n## Construction Model (RAII) \u0026 Ephemeral Ports\n\nOverview relocated to: [Construction Model (RAII \u0026 Ephemeral Ports)](docs/FEATURES.md#construction-model-raii--ephemeral-ports)\n\n## TLS Features (Current)\n\nSee: [TLS Features](docs/FEATURES.md#tls-features)\n\n### TLS Metrics Reference\n\nMetrics example: [TLS Features](docs/FEATURES.md#tls-features)\n\n## OpenTelemetry Support (Experimental)\n\nAeronet provides optional OpenTelemetry integration for distributed tracing and metrics. Enable with the CMake flag `-DAERONET_ENABLE_OPENTELEMETRY=ON`. Be aware that it pulls also `protobuf` dependencies.\n\n### Architecture\n\n**Instance-based telemetry:** Each `SingleHttpServer` maintains its own `TelemetryContext` instance. There are no global singletons or static state. This design:\n\n- Allows multiple independent servers with different telemetry configurations\n- Eliminates race conditions and global state issues\n- Makes testing and multi-server scenarios straightforward\n- Ties telemetry lifecycle directly to server lifecycle\n\nAll telemetry operations log errors via `log::error()` for debuggability—no silent failures.\n\n### Dependencies\n\nWhen OpenTelemetry is enabled, aeronet requires the following system packages:\n\n**Debian/Ubuntu:**\n\n```bash\nsudo apt-get install libcurl4-openssl-dev libprotobuf-dev protobuf-compiler\n```\n\n**Alpine Linux:**\n\n```bash\napk add curl-dev protobuf-dev protobuf-c-compiler\n```\n\n**Fedora/RHEL:**\n\n```bash\nsudo dnf install libcurl-devel protobuf-devel protobuf-compiler\n```\n\n**Arch Linux:**\n\n```bash\nsudo pacman -S curl protobuf\n```\n\n### Configuration Example\n\nConfigure OpenTelemetry via `HttpServerConfig`:\n\n```cpp\n#include \u003caeronet/aeronet.hpp\u003e\n\nusing namespace aeronet;\n\nint main() {\n  HttpServerConfig cfg;\n  cfg.withPort(8080)\n     .withTelemetryConfig(TelemetryConfig{}\n                              .withEndpoint(\"http://localhost:4318\")  // OTLP HTTP endpoint\n                              .withServiceName(\"my-service\")\n                              .withSampleRate(1.0)  // 100% sampling for traces\n                              .enableDogStatsDMetrics());  // Optional DogStatsD metrics via UDS\n  \n  SingleHttpServer server(cfg);\n  // Telemetry is automatically initialized when server.init() is called\n  // Each server has its own independent TelemetryContext\n  \n  // ... register handlers ...\n  server.run();\n}\n```\n\n### Built-in Instrumentation\n\nWhen OpenTelemetry is enabled, aeronet automatically tracks:\n\n**Traces:**\n\n- `http.request` spans for each HTTP request with attributes (method, path, status_code, etc.)\n\n**Metrics (non exhaustive list):**\n\n- `aeronet.events.processed` – epoll events successfully processed per iteration\n- `aeronet.connections.accepted` – new connections accepted\n- `aeronet.bytes.read` – bytes read from client connections\n- `aeronet.bytes.written` – bytes written to client connections\n\nAll instrumentation happens automatically—no manual API calls required in handler code.\n\n### Query String \u0026 Parameters\n\nDetails here: [Query String \u0026 Parameters](docs/FEATURES.md#query-string--parameters)\n\n### Logging\n\nDetails moved to: [Logging](docs/FEATURES.md#logging)\n\n## Streaming Responses (Chunked / Incremental)\n\nMoved to: [Streaming Responses](docs/FEATURES.md#streaming-responses-chunked--incremental)\n\n### Mixed Mode \u0026 Dispatch Precedence\n\nMoved to: [Mixed Mode \u0026 Dispatch Precedence](docs/FEATURES.md#mixed-mode--dispatch-precedence)\n\n## Configuration API (builder style)\n\n`HttpServerConfig` lives in `aeronet/http-server-config.hpp` and exposes fluent setters (withX naming):\n\n```cpp\nHttpServerConfig cfg;\ncfg.withPort(8080)\n  .withReusePort(true)\n  .withMaxHeaderBytes(16 * 1024)\n  .withMaxBodyBytes(2 * 1024 * 1024)\n  .withKeepAliveTimeout(std::chrono::milliseconds{10'000})\n  .withMaxRequestsPerConnection(500)\n  .withKeepAliveMode(true);\n\nSingleHttpServer server(cfg); // or SingleHttpServer(8080) then server.setConfig(cfgWithoutPort);\n```\n\n### Handler Registration / Routing (Detailed)\n\nTwo mutually exclusive approaches:\n\n1. Global handler: `router.setDefault([](const HttpRequest\u0026){ ... })` (receives every request if no specific path matches).\n2. Per-path handlers: `router.setPath(http::Method::GET | http::Method::POST, \"/hello\", handler)` – exact path match.\n\nRules:\n\n- Mixing the two modes (calling `setPath` after `setDefault` or vice-versa) throws.\n- If a path is not registered -\u003e 404 Not Found.\n- If path exists but method not allowed -\u003e 405 Method Not Allowed.\n- You can call `setPath` repeatedly on the same path to extend the allowed method mask (handler is replaced, methods merged).\n- You can also call `setPath` once for several methods by using the `|` operator (for example: `http::Method::GET | http::Method::POST`)\n\nExample:\n\n```cpp\nRouter router;\nrouter.setPath(http::Method::GET | http::Method::PUT, \"/hello\", [](const HttpRequest\u0026){\n  return HttpResponse(200).body(\"world\");\n});\nrouter.setPath(http::Method::POST, \"/echo\", [](const HttpRequest\u0026 req){\n  return HttpResponse(200).body(req.body());\n});\n// Add another method later (merges method mask, replaces handler)\nrouter.setPath(http::Method::GET, \"/echo\", [](const HttpRequest\u0026 req){\n  return HttpResponse(200).body(\"Echo via GET\");\n});\n```\n\n### Limits\n\n- **431** is returned if the header section exceeds `maxHeaderBytes`.\n- **413** is returned if the declared `content-length` exceeds `maxBodyBytes`.\n- Connections exceeding `maxOutboundBufferBytes` (buffered pending write bytes) are marked to close after flush (default 4MB) to prevent unbounded memory growth if peers stop reading.\n- Slowloris protection: configure `withHeaderReadTimeout(ms)` to bound how long a client may take to send an entire request head (request line + headers) (0 to disable). `aeronet` will return HTTP error **408 Request Timeout** if exceeded.\n\n### Performance / Metrics \u0026 Backpressure\n\n`SingleHttpServer::stats()` exposes aggregated counters:\n\n- `totalBytesQueued` – bytes accepted into outbound buffering (including those sent immediately)\n- `totalBytesWrittenImmediate` – bytes written synchronously on first attempt (no buffering)\n- `totalBytesWrittenFlush` – bytes written during later flush cycles (EPOLLOUT)\n- `deferredWriteEvents` – number of times EPOLLOUT was registered due to pending data\n- `flushCycles` – number of flush attempts triggered by writable events\n- `maxConnectionOutboundBuffer` – high-water mark of any single connection's buffered bytes\n\nUse these to gauge backpressure behavior and tune `maxOutboundBufferBytes`. When a connection's pending buffer would exceed the configured maximum, it is marked for closure once existing data flushes, preventing unbounded memory growth under slow-reader scenarios.\n\n### Metrics Callback (Scaffold)\n\nYou can install a lightweight per-request metrics callback capturing basic timing and size information:\n\n```cpp\nSingleHttpServer server;\nserver.setMetricsCallback([](const SingleHttpServer::RequestMetrics\u0026 m){\n  // Export to stats sink / log\n  // m.method, m.target, m.status, m.bytesIn, m.bytesOut (currently 0 for fixed responses), m.duration, m.reusedConnection\n});\n```\n\nCurrent fields (alpha – subject to change before 1.0):\n\n| Field | Description |\n|-------|-------------|\n| method | Original request method string |\n| target | Request target (decoded path) |\n| status | Response status code (best-effort 200 for streaming if not overridden) |\n| bytesIn | Request body size (after chunk decode) |\n| bytesOut | Placeholder (0 for now, future: capture flushed bytes per response) |\n| duration | Wall time from parse completion to response dispatch end (best effort) |\n| reusedConnection | True if this connection previously served other request(s) |\n\nThe callback runs in the event loop thread – keep it non-blocking.\n\n### Test HTTP Client Helper\n\nThe test suite uses a unified helper for simple GETs, streaming incremental reads, and multi-request keep-alive batches. See `docs/test-client-helper.md` for guidance when adding new tests.\n\n### Roadmap additions\n\n- [x] Connection write buffering / partial write handling\n- [x] Outgoing chunked responses \u0026 streaming interface (phase 1)\n- [x] Trailing headers exposure for chunked requests\n- [x] Richer routing (wildcards, parameter extraction)\n- [x] TLS (OpenSSL) support (basic HTTPS termination)\n- [x] Benchmarks \u0026 perf tuning notes\n\n### TLS (HTTPS) Support\n\nDetails merged into: [TLS Features](docs/FEATURES.md#tls-features)\n\n## Acknowledgements\n\nCompression libraries (zlib, zstd, brotli), OpenSSL, Opentelemetry and spdlog provide the optional feature foundation; thanks to their maintainers \u0026 contributors.\n\n## License\n\nLicensed under the MIT License. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsjanel%2Faeronet","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsjanel%2Faeronet","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsjanel%2Faeronet/lists"}