Skip to content

feat(response): the non-blocking dialect twins, and four defects they exposed (#177) - #192

Merged
EdmondDantes merged 1 commit into
mainfrom
178-dialect-twins
Aug 21, 2026
Merged

feat(response): the non-blocking dialect twins, and four defects they exposed (#177)#192
EdmondDantes merged 1 commit into
mainfrom
178-dialect-twins

Conversation

@EdmondDantes

Copy link
Copy Markdown
Contributor

Closes#177.

HttpResponse::trySseEvent() and tryWriteMessage() frame exactly what sseEvent() and
writeMessage() frame and answer false on a full queue, having queued nothing and
committed no header, so the same event or message can be offered again. Until now the
idiom was half-applied: a handler streaming SSE or gRPC had tryWrite() in the API and no
way to use it, because the framing lives inside the dialect call.

Both dialects share their formatting and their guards with the blocking twin — one helper
per dialect, and the entry points differ in the flag they pass to append_chunk.

Four defects the work exposed, fixed here

  • awaitWritable() was refused in SSE mode, so a handler refused by trySseEvent()
    had nowhere to wait. Waiting emits nothing, so that guard and the buffered-body one now
    apply to the calls that emit.
  • writeMessage() answered to neither guard. A gRPC frame could be pushed into a
    text/event-stream body, or silently discard a body set with setBody(). It now goes
    through the same check as write(), and so does the new twin.
  • trySseEvent() started the stream before probing the peer, so its 499 landed on a
    committed response and the handler could not answer with a status. It probes first.
  • awaitWritable($timeoutMs) documented a deadline no transport read. HTTP/2 now
    waits the shorter of the argument and the connection's write timeout.

Evidence

  • h1/031: HTTP/1 accepts every offer — it keeps no queue — and the record matches
    sseEvent() byte for byte, including the all-arguments-null no-op.
  • h2/026: the ring fills, one refusal is taken and waited out, and the body is identical
    to the blocking twin's — which is what proves a refusal queued nothing.
  • h2/027: the same for gRPC, alternating the two twins so the body also proves they
    frame alike, five-byte prefix included.
  • h1/032: the peer aborts with a RST and both twins throw HttpException 499 rather
    than answering false, on a response that is still uncommitted.
  • Full suite: 285 passed, 0 failed, 1 pre-existing warn.

Also removes the #179 promise from three comments and the tryWrite docblock: the
measurement in #191 replaced it with a documented exception.

@github-actions

Copy link
Copy Markdown
Contributor

Coverage

Total lines: 82.14% → 82.11% (-0.03 pp)

FileBaselineCurrentΔTouched
src/core/http_connection.c75.58%75.39%-0.19 pp
src/core/http_connection_tls.c73.74%73.46%-0.28 pp
src/http1/http1_stream.c50.35%50.35%+0.00 pp
src/http2/http2_strategy.c84.13%84.16%+0.04 pp
src/http3/http3_callbacks.c82.19%82.30%+0.11 pp
src/http3/http3_listener.c78.38%77.03%-1.35 pp
src/http3/http3_packet.c90.43%85.22%-5.22 pp
src/http_response.c87.50%88.28%+0.78 pp
src/http_server_config.c92.47%92.41%-0.06 pp
src/http_sse.c85.11%85.45%+0.35 pp
src/websocket/ws_session.c89.67%90.04%+0.37 pp

@EdmondDantes
EdmondDantes merged commit 12bfcb7 into mainAug 21, 2026
8 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A streaming handler cannot offer a chunk without committing to the wait

1 participant

@EdmondDantes