Skip to content

HTTP Streaming

github-actions[bot] edited this page Sep 9, 2026 · 6 revisions

HTTP Streaming

Implemented Scope

Elio's HTTP/1 receive path provides a shared incremental response decoder and a pull reader. Ordinary HTTP clients explicitly accumulate its body slices; SSE clients incrementally parse those slices as events. This is the receive phase of #1191, tracked in #1192.

The sending path uses a shared response plan and server-managed producer/writer lifecycle. This is HTTP/1 behavior, not an HTTP/2 streaming API or an end-to-end zero-copy guarantee.

Outgoing Response Ownership

response owns a complete body. Its constructors and set_body() change body storage without generating Content-Length in the metadata. response_head contains metadata only. A move-only streaming_response owns its producer and declares either a known byte count or an unknown length. reply is the variant of complete, streaming, and tunnel responses. Tunnel acceptance has a separate handoff lifecycle described below; it is not an ordinary response body.

Router registrations accept synchronous or task results of all four reply shapes through explicit adapters. Registered handler callables are copyable; the producer returned by a handler may be move-only. A context remains alive through producer execution and send cleanup. Once the handler selects its final reply, send_interim() is sealed and returns false with EALREADY.

The server invokes a producer at most once. HEAD and body-forbidden statuses skip it. Open files, authorize access, and do work that can change final status before returning a streaming response. A failure after headers cannot be repaired by sending another final response on that connection.

The producer receives body_writer& and a cooperative cancellation token. It returns task<send_result>. Each write/writev is sequential and borrowed: keep both descriptor storage and payload valid and immutable until the await returns, including failure cleanup. Never detach a write, escape the writer, or use it concurrently. Empty writes do not end the response. Only successful producer return delegates final framing to the server; there is no public finish() operation.

CONNECT Tunnel Handoff

Register router.connect(handler) for authority-form CONNECT requests. The handler receives (context&, connect_authority_view); the authority contains borrowed raw and host spellings plus a parsed uint16_t port. This is a dedicated authority handler, not a path route. Syntax validation does not authorize, resolve, percent-decode, or connect to the destination. Copy raw and host into owning strings if they must outlive the handler/session context; copying a view does not extend the lifetime of its backing storage.

Return an ordinary non-2xx response to reject a CONNECT, or a tunnel_response owning a callable with signature task<tunnel_result>(tunnel_stream&, coro::cancel_token) to accept it. The session callable may be move-only. A tunnel response defaults to 200 and may customize its response head, but acceptance requires HTTP/1.0 or HTTP/1.1, CONNECT, a 2xx status, and neither Content-Length nor Transfer-Encoding. An ordinary complete/streaming 2xx CONNECT reply is rejected by the sender; do not use an empty ordinary response to establish a tunnel.

The server completely writes the acceptance head before invoking the session, at most once. It moves the parser's captured post-header bytes into the scoped tunnel_stream; reads consume this binary prefix once before reading the transport. Those bytes are never passed to a second HTTP parser. No HTTP body producer, chunk encoder, or final chunk marker participates. Acceptance ends HTTP processing permanently; the connection is never returned to HTTP reuse. Rejected CONNECT requests also close conservatively, so speculative tunnel bytes cannot become a following HTTP request.

The view is noncopyable/nonmovable and borrows the original TCP or TLS transport. One reader may overlap one write-side operation. The callback must join all operations it starts before returning; neither the view nor active operations may escape. Keep write payload and iovec descriptor storage valid and immutable until the await returns. write() and writev() complete the submitted slices or return a terminal error with accepted_bytes and uncertain_attempt. Confirmed positive progress is retained. An attempted but unconfirmed suffix is not known to be lost, so do not replay it automatically. Completion means local transport acceptance, not peer application receipt. Failed reads and output finish report the view's first terminal error, so cancellation used to clean up a sibling does not hide the original failure. Successful positive read/write progress remains available for accounting.

Handoff does not materialize tunnel plaintext as an HTTP body or aggregate write payloads. The parser prefix was already captured and is moved into the view; subsequent reads copy its bytes into caller storage. TLS still has its own cryptographic buffers and bounded ciphertext staging. This is not a claim of zero-copy TLS or zero allocation.

Optional Bounded Relay

Tunnel read, write, writev and finish_output entries may throw before a task is returned if frame construction fails. They first retain a terminal error and request sibling cancellation. Catching the exception cannot restore the view: join overlapping work before releasing its buffers or ending the callback. An allocation failure creating the nested vector operation inside an already started scalar write is returned as a structured ENOMEM failure, with zero accepted bytes and no uncertain transport attempt. Cancellation remains cooperative; neither failure form permits destruction of live coroutine frames.

relay(tunnel_stream&, net::stream&, relay_options{}, token) uses exactly two fixed payload buffers, each relay_options::buffer_size bytes (64 KiB by default). It owns and joins both directional tasks, including cancellation, exceptions and partial child-launch failure. It does not resolve or connect the upstream, add a total-lifetime timer, or impose a reverse-half-close timer.

TCP and TLS 1.3 directional EOF finishes the opposite destination's output without cancelling healthy reverse traffic. read_end_scope() describes the observed closure, rather than requiring applications to branch on TLS version. TLS 1.2 whole-session closure instead stops the relay after close coordination and joined cleanup. A writer encountering normal whole-session ESHUTDOWN joins the existing destination close driver before stopping its sibling; it must not cancel that driver while its response alert is still draining.

TLS 1.2 freezes new plaintext submission at closure. Already BIO-accepted ciphertext is committed: it remains ordered before the close alert, not dropped or reordered around it. One whole-session close deadline, five seconds by default, covers that process; repeated joins do not restart it. Resource exhaustion, an unsafe unfinished SSL write retry, cancellation or expiry can abort closure. Neither session_closed nor a successful close result promises that all reverse plaintext was forwarded. The five-second budget does not limit normal TCP/TLS 1.3 reverse-half-close lifetime.

Relay direction results distinguish source bytes read, bytes confirmed by successful destination writes, and uncertain failed attempts. Counter overflow is terminal. completed and session_closed are normal termination categories, not delivery acknowledgments or application-level success. Internal sibling stop is distinguished from the external cancellation/failure that selected the relay result. The relay also observes inherited task cancellation before starting either direction. A child-startup exception remains a failure even if its cleanup cancels another child; that internal cancellation does not hide the cause.

See the canonical CONNECT proxy example. Authorization, allowed destinations/ports, DNS resolution and rebinding policy, upstream connection policy, resource limits and application deadlines remain caller responsibilities. For an HTTPS proxy the tunnel retains the original outer TLS connection; it never detaches a plaintext socket. Inner TLS bytes remain opaque tunnel payload. Selecting TLS for an upstream leg is an explicit application choice, not an automatic consequence of CONNECT or port 443.

Framing And Failure

Response Effective framing
Complete body or known-length stream Derived/declared Content-Length, with exact byte-count enforcement
Unknown-length HTTP/1.1 stream Chunked
Unknown-length HTTP/1.0 stream Close-delimited, no connection reuse
Explicit close-delimited stream No CL/TE, no connection reuse; only for unknown length
HEAD No body or producer invocation; length describes the representation
204 No body or framing length
205 No body, Content-Length: 0
304 No body; optional explicitly supplied representation length

Manual Content-Length is a validated assertion, not an encoder switch. Invalid or conflicting assertions and manually supplied Transfer-Encoding are rejected before final output. Ordinary final planning rejects 1xx and successful CONNECT; interims and WebSocket 101 handoff have separate paths.

Representation metadata is a caller assertion, not a verified hypothetical GET result. Subject to status precedence, HEAD selects explicit representation length, then known body length; manual CL must match and is not a fallback for an unknown length. A 304 instead selects explicit representation length, then manual CL. The caller must supply the correct selected-representation size: Elio checks syntax/consistency without invoking the suppressed producer.

Logical writes absorb short writes and EINTR; readiness-aware transports handle EAGAIN without busy spinning. No caller remainder retry or whole-response replay is required or supported. An unrecoverable error permanently terminates the writer. Too-long production is rejected before sending the excess; too-short production fails finalization. Failure prevents deliberate emission of a normal chunk terminator and prevents HTTP connection reuse.

confirmed_body_bytes is cumulative diagnostic progress, not a replay offset. It can increase when an underlying completion arrives after timeout wins. The winning cancellation/timeout/error result remains terminal; already-sent bytes cannot be rolled back and may already have reached the peer.

server_config::write_timeout defaults to zero (disabled). Positive values apply to each logical write under one original deadline across partial progress, not to the whole response or an SSE stream. Timeout requests abort; return still waits for transport and watchdog cleanup. It is not a hard upper bound on return time. Coroutine-frame allocation can throw before returning a task; the writer still becomes terminal, even if the producer catches that exception.

Selection, Failure And Recovery

Stage / event Server behavior Application responsibility
Handler has not selected a final reply A standard exception invokes the configured error handler, or selects 500; a failing error handler or non-standard exception selects 500. Open/validate sources and choose recoverable application errors here. Previously sent interim responses do not constitute a final response.
A reply is selected but preflight rejects its framing No final response bytes are emitted; the connection closes without selecting a replacement reply. Supply coherent metadata. Preflight rejection is not a request to retry or call the error handler.
Final headers are being sent The final selection is sealed; write failure terminates the response and disables reuse. Do not send another final status or an interim response through the sealed context.
Producer runs after final headers Successful logical writes complete their submitted bytes; a producer exception/error or terminal write failure ends the response. Keep borrowed storage alive through awaited cleanup, propagate failure and do not replay the response. A local exception catch cannot revive a failed writer.
Producer returns successfully and writer has not failed The server verifies the declared length and completes the selected framing. A previously failed writer remains terminal even if its result was ignored. Do not call a competing finish operation or assume producer return alone proves response success.
Timeout/cancellation wins while I/O is in flight Further submission stops; the server waits for cleanup before releasing borrowed state. Partial or even complete peer receipt remains possible. Reuse buffers only after the awaited operation returns; handle business-level retry/idempotency separately.

The error-handler fallback belongs to handler execution, not to every later failure. In particular, neither preflight failure nor producer failure appends a replacement 500 response. Destruction does not finish a response implicitly.

Sending Costs And Shutdown Boundaries

The HTTP writer uses bounded iovec cursors and small chunk metadata, not a body-sized serialization buffer. TCP uses scatter/gather; TLS consumes borrowed segments through its scalar encrypted-write path. TLS record/encryption buffers, kernel copies, and coroutine/control allocations remain separate costs. This contract does not claim allocation-free writes or one syscall/chunk/record per logical write. No hidden queue retains body bytes between calls.

Extension Boundaries

The initial writer accepts borrowed memory, not file ranges. A producer can currently read a file into a bounded reusable buffer and await each write. Direct file transfer remains a future transport capability, not a promise of this release. Its design boundary is a writer operation carrying an owned or borrowed file handle plus explicit offset/count, dispatched to a capable transport or an explicitly bounded fallback. It must share the existing length accounting, framing, deadline arbitration and cleanup-before-return rules; it must not bypass the writer by writing raw bytes to its underlying socket. TLS and unsupported backends must not silently claim a zero-copy path. This extension does not require changing producer ownership or adding a detached send queue; the exact public signature remains deferred.

Outgoing trailers are also deferred: the server emits the normal empty trailer section when it successfully finishes a chunked response. Setting a Trailer header does not supply trailer values or create a trailer callback. Receive-side trailer parsing is a separate capability and does not imply a sending API.

stop() cooperatively cancels HTTP sessions and producers; a producer ignoring its token cannot be safely forcibly destroyed. Before destroying a server or its TLS context, request stop, await all listener tasks, and then wait for active connections to reach zero. A zero count observed before listener completion does not exclude an accepted connection about to be registered.

WebSocket ordinary HTTP fallback uses the same sender but does not reuse the connection. Once 101 has successfully handed ownership to WebSocket, its handler retains its separate shutdown contract. HTTP producer cancellation is not a promise to terminate upgraded handlers. TLS close-notify also retains its existing bounded shutdown timeout rather than a new hard cancellation guarantee.

Managed SSE

Return sse::make_streaming_response(producer) from a normal route. Its producer receives a scoped sequential sse::event_writer& and cancellation token. send_event(event_view), send_data(string_view), and send_comment(string_view) borrow field slices through completion and use bounded descriptors for line prefixes/delimiters. HTTP chunking is applied by the shared writer, not by the SSE producer. Invalid id/type control characters fail before event output; ignored sink failures still prevent successful response finalization.

event_view::id distinguishes absence from an empty value: std::nullopt or {} omits id, while std::string_view{} or "" emits id:\n to clear the receiver's Last-Event-ID. Subsequent omitted IDs preserve that cleared state. The optional borrows its string view; send_data() continues to omit id.

The factory supplies Content-Type: text/event-stream and Cache-Control: no-cache. It does not grant cross-origin access by default. Set an appropriate CORS policy explicitly. The former build_sse_response() header-only helper is removed. Legacy raw-stream sse_connection is not a managed HTTP body writer and must not be used to bypass framing on a managed response.

Data Path And Ownership

transport → reusable reader buffer → HTTP body view → application / SSE parser
                                    metadata only → bounded decoder storage

response_decoder::decode(input) returns body views into input, never an owned body copy. It can deliver the beginning of a chunk without receiving its remaining payload or trailing CRLF. It owns parsed headers and fragmented framing lines, not a whole-message or whole-chunk buffer.

response_reader owns one reusable transport buffer and a decoder. Its read(stream, token) returns one event per await. The body view remains valid until the next reader operation, move, or destruction. Consume it synchronously, or await a borrowing downstream operation before calling read() again. Copy explicitly if data must outlive that boundary or be processed independently.

The reader is move-only. Move it only without an active operation, and treat the moved-from object as usable only for destruction or assignment. The stream is supplied per read, not captured permanently, and must remain alive and unmoved until that read completes. Do not switch streams midway through a response. Serialize reads, configuration changes, resets, and moves.

The default receive buffer is 8192 bytes; a requested size of zero becomes one byte. Parsed metadata is bounded separately by 100 field lines and 8192 bytes per line by default. Status, chunk-size and trailer lines share the line-size limit; trailers and duplicate headers consume the field-count budget. A fragmented line may retain its trailing CR in addition to its allowed content. Declared chunks retain the existing 1 GiB upper bound. There is no implicit aggregate body limit in the decoder or reader: choose one at the application layer when accumulating data. TLS and kernel transport buffering are outside this HTTP-layer no-body-copy guarantee; SSE event parsing still owns its event strings and applies its configured event-buffer limit.

Events And Message Boundaries

Event Meaning and caller action
need_more Decoder accepted all current input and needs more bytes. The reader handles this internally.
headers_complete Metadata and framing are available. Inspect status before consuming body.
body A nonempty borrowed payload fragment; not an HTTP chunk or SSE event boundary.
message_complete The framed message ended successfully; it may be an interim response.
protocol_handoff A 101 or successful CONNECT header boundary; validate the upgrade/tunnel separately.
error Stop normal message processing and inspect the decoder diagnostic or reader error code.

Headers are reported before body even when both arrive in one transport read. For direct decoder use, advance the current input by result.consumed and decode again, including with empty input, until need_more or a terminal event. Terminal events repeat with zero consumption. Unlike response_parser, this consumed count never includes bytes from an earlier feed.

After a completed interim, reader.next_response() retains any unread bytes and starts another message. It clears method context: set the original request method again. It refuses pending, erroneous, and handed-off messages. reader.reset() is different: it discards unread data and EOF state for a new connection, retaining buffer capacity and configured limits. Neither closes or reopens a connection. Both must run without an active read.

remaining() exposes unread wire data. Use it for a validated protocol handoff, not as SSE/body data. Low-level code owns connection reuse decisions; ordinary http::client refuses reuse for close-delimited responses or buffered bytes after a final response.

HEAD has no delivered body even if Content-Length advertises representation size. Informational/204/304 responses complete at their header boundary. Elio also treats 205 as bodyless. Other responses use Content-Length, supported chunked encoding, or connection close. Only the single chunked transfer coding is implemented; stacks such as gzip, chunked are rejected, not partially decoded and mislabeled as body. This is separate from application handling of Content-Encoding.

Completion, Errors And Cancellation

response_read_result::success() means error == 0, not that the whole response is complete. Normal EOF completes a close-delimited response. EOF before a declared length, chunk delimiter, or final trailer terminator is EBADMSG. Configured metadata-limit failures are EMSGSIZE; transport errors remain positive error codes. Payload may have been delivered before a later framing failure. Do not infer a valid complete document from those earlier slices.

read() requires a readiness-aware stream and handles short reads and EINTR internally. It does not busy-retry EAGAIN from a transport that violates that precondition. read_with(receive, token) lets a caller supply a deadline-aware read policy without duplicating HTTP parsing. The callback is invoked with (void* buffer, size_t capacity) and must return an awaitable io::io_result, write no more than capacity, and stop accessing the buffer before returning. Capture/pass cancellation into that callback's transport operation explicitly; the reader does not pass the token as a third callback argument.

Transport errors preserve parsing state, which allows the HTTP client's Expect wait to cancel a pending read and continue when the transport permits it. This is not permission to retry arbitrary failed transports. Framing errors are terminal until reset. The reader does not impose a timeout, close a stream, pool a connection, automatically reconnect, or replay application work.

Cancellation remains cooperative. Safe return relies on the underlying read finishing its access to the borrowed buffer; do not destroy an in-flight reader or coroutine frame to force timeout completion. A buffered event can be returned without issuing a new transport read. Callers requiring cancellation to suppress already-buffered delivery should check their token before pulling.

Borrowing A Body Fragment Through A Downstream Await

This helper assumes the source connection already has a request awaiting its response. It forwards decoded body to a raw byte sink, not to an HTTP response writer. The caller owns connection cleanup and application policy.

#include <elio/elio.hpp>
#include <elio/http/http_response_reader.hpp>

elio::coro::task<bool> forward_body(
    elio::net::stream& source, elio::net::stream& sink,
    elio::coro::cancel_token token) {
    using namespace elio::http;
    response_reader reader;
    reader.set_request_method(method::GET);
    size_t interims = 0;
    for (;;) {
        if (token.is_cancelled()) co_return false;
        const auto part = co_await reader.read(source, token);
        if (!part.success()) co_return false;
        if (part.event == response_event::body) {
            const auto sent = co_await sink.write_exactly(
                part.body.data(), part.body.size(), token);
            if (sent.result < 0 ||
                static_cast<size_t>(sent.result) != part.body.size()) {
                co_return false;
            }
        } else if (part.event == response_event::protocol_handoff) {
            co_return false;
        } else if (part.event == response_event::message_complete) {
            if (reader.decoder().status_code() >= 200) co_return true;
            if (++interims > 16 || !reader.next_response()) co_return false;
            reader.set_request_method(method::GET);
        }
    }
}

The next source read occurs only after the sink write completes, so the borrowed body remains valid and the downstream await supplies backpressure. A later source-framing or sink error cannot undo bytes already forwarded. This helper does not promise application-level atomicity or infer that a 4xx/5xx status is application success.

Validation Map

Canonical Sending Example

Build http_streaming_server with HTTP/TLS examples enabled and run it with a configured regular file:

./build/examples/http_streaming_server --port 8080 --file ./sample.bin

It binds only to loopback; the request URL cannot select a filesystem path. Open/type/size validation precedes reply selection, while file reads use a 64 KiB borrowed buffer on the blocking pool. Keep the source contents stable during a response: a measured file length is not an immutable file snapshot. Shrinking the file can fail the producer after headers have started.

Request Demonstrated contract
GET /empty Complete empty response with truthful length
GET /file Known-length file producer with bounded storage and awaited backpressure
HEAD /file Same representation length, no producer invocation or body read
GET /file-invocations Observable count for checking HEAD suppression
GET /failure Prefix followed by producer failure; no successful chunk terminator or replacement response
GET /cancel, followed by SIGINT/SIGTERM Token-aware producer and signal-driven listener/session drain

http_streaming_example.py runs these behaviors with a generated file and retained process logs in the focused CI workflow. Migration snippets have their own compiler-checked current-side translation unit, tests/compile/http_streaming_migration.cpp. The managed SSE example is examples/sse_server.cpp; its event sink uses the same writer lifecycle.

Evidence By Surface

Sending and receiving have separate evidence. A passed plain-loopback peer test does not establish TLS, backend, sanitizer, or throughput coverage.

  • test_http_response_plan.cpp: method/status/version framing, explicit length assertions, serializer parity, metadata preservation and move-only ownership.
  • test_http_body_writer.cpp: controlled short writes, borrowed pointer lifetime, terminal arbitration, late completion, watchdog cleanup and allocation-failure checkpoints. Checkpoints are not a replacement for real allocator/sanitizer execution.
  • test_http_response_sender.cpp: ordinary and owned producer execution, HEAD suppression, repeat dispatch, producer failure and length enforcement.
  • test_http_streaming_server.cpp and test_http_server.cpp: route adapters, context lifetime/sealing, raw wire, connection reuse and cooperative stop.
  • test_http_streaming_transport.cpp: real loopback TCP/TLS writes against a non-draining peer, cancellation/timeout after observed pending I/O, sticky failure, no finalizer write and buffer reuse after cleanup. Forced epoll and io_uring cases report unavailable backends explicitly.
  • test_sse_writer.cpp: multiline/UTF-8 and metadata validation, fixed descriptor batches, borrowed source identity, sticky failure and factory ownership.
  • http_streaming_peer.py with http_streaming_peer_server.cpp: pinned h11 0.16.0 validates plain HTTP/1.1 sequential connection reuse, complete/known/ chunked replies, HEAD producer suppression, 204/205/304 metadata, finite SSE and truncated failure. Enable ELIO_BUILD_HTTP_INTEROP_TESTS and install the exact peer version in the selected Python environment; CTest runs http_streaming_interop.
  • http_streaming_cost_probe.cpp: complete/known/chunked sends of prepared 64 KiB and 4 MiB bodies into a controlled, nonbuffering sink. Counts successful C++ allocation requests on the measuring thread and checks exact in-order borrowed source coverage and bounded iovecs. Excludes prepared source storage, malloc/custom allocator bypasses, other threads and transport/kernel buffers. Cumulative allocation bytes are not peak memory or throughput. Pointer coverage proves borrowed transport inputs, not absence of every possible intermediate copy; control/coroutine allocations remain observable.

The HTTP streaming contracts workflow also runs for relevant wiki edits and compiles public-header and SSE examples. It publishes a per-scenario table and retains peer logs. Its scope is conformance, not performance ranking; sanitizer coverage belongs to the main Debug matrix, not this focused peer job. Runtime results must still be checked for the current PR head before merging.

  • test_http_response_decoder.cpp: pointer identity, partial large chunks, every split/bytewise framing, EOF, limits, HEAD, handoff and adapter semantics.
  • test_http_response_reader.cpp: reusable-buffer pulls, error/EOF handling, interim boundaries, lifetime and state transitions.
  • test_sse.cpp: genuine chunked and length-delimited wire fixtures, UTF-8 and event delimiters split across chunks, truncation and post-body bytes.
  • test_http_client.cpp: ordinary and Expect response paths and connection behavior. Receiving final headers must suppress the pending upload without waiting for its response body.

See API Contracts, API Reference, WebSocket SSE, and Migrating to 0.6 for related contracts and migration instructions.

Real TCP/TLS Diagnostics

The opt-in ELIO_BUILD_HTTP_METRICS fixture reports six sequential coordinates: TCP and TLS, each with complete, known-length streaming and chunked responses. The transport-metrics job builds it in Release mode and publishes a readable table with raw JSON and process logs. These are diagnostic observations with performance_eligible=false, not rankings or a shared-runner regression gate.

Each coordinate uses a separate Elio server process and pinned h11 0.16.0 client. One connection carries an excluded warmup, sixteen verified 4 MiB responses, then a small ordinary response proving the last measured response left a reusable boundary. Streaming modes submit sequential borrowed 64 KiB slices; complete responses submit an owned body. These different submission shapes are part of the workload, not an assertion that the modes are equivalent implementations or that one is faster.

Reported throughput is verified loopback pipeline throughput: decoded body bytes divided by client monotonic elapsed time from the first measured request through final message validation. Requests, socket transfers, TLS, h11 parsing and incremental integrity verification are included. The client can be the bottleneck; this does not measure isolated server capacity. Startup, certificate generation, connection/handshake, warmup and the final reuse probe are excluded.

Server CPU is the sum of process CPU intervals around measured send_response calls, including server process activity during those intervals. Prepared body storage and response setup are outside those windows. Client CPU uses the separate client process over its measured interval and includes parsing, decryption and verification. CPU scopes differ from the wall-clock scope and must not be interpreted as a common utilization denominator.

Wire trial identifiers, sequence numbers, framing, body length/digest, sender completion and final connection reuse are checked before a coordinate passes. Missing, duplicate or mismatched evidence is incomplete, not a successful row. On a client request failure, client.json retains failed_response with the trial, phase, sequence, parsed-final-header state, received HTTP plaintext bytes, decoded body bytes, elapsed nanoseconds, operation and exception type/message. plaintext_bytes counts bytes returned by TCP/TLS recv, including HTTP headers and framing; body_bytes counts only h11-decoded body events. Partial headers or unparsed bytes can therefore increase the former without increasing the latter. These fields are diagnostics, not additional verified responses or throughput.

Failed server result events include a failure object with the active stage, expected request phase/sequence, received request bytes, request completion and elapsed time, the last returned read result/flags (or null if that await did not return), exception category/message, and whether phase or trial supervision expired. The request fields describe the last request, including when the failure occurs during its response send. If cancellation cleanup itself cannot finish, a separate failure event retains the supervision cause without reading coroutine-owned evidence concurrently. Raw stdout and stderr remain unchanged artifacts alongside parsed JSON. Diagnostics neither extend deadlines nor turn partial evidence into successful coverage.

The intermittent TLS timeout tracked in #1219 remains an unresolved investigation; improved evidence retention is not a root-cause correction or proof that later passing runs exclude a library, fixture or infrastructure defect. The reports retain build and transport metadata; shared-runner state and peer verification costs remain limitations. The controlled-sink allocation probe above is a separate measurement: it does not provide TCP/TLS allocation totals, and these transport timings do not prove end-to-end zero-copy.

For a local run, configure an out-of-source Release build with tests, HTTP and TLS enabled and -DELIO_BUILD_HTTP_METRICS=ON. Install h11==0.16.0 into an isolated Python environment and select it through Python3_EXECUTABLE. Build elio_http_streaming_metrics_server, run the http_streaming_metrics_report_tests CTest, then invoke:

python tests/integration/http_streaming_metrics.py \
  --server /path/to/build/tests/elio_http_streaming_metrics_server \
  --output-dir /path/to/results

The driver requires the OpenSSL command-line tool for a local test certificate. It uses bounded deadlines and preserves failure evidence. No throughput number or small timing fluctuation determines conformance success.

Clone this wiki locally