Skip to content

nghttp2: streamed response bodies, and the ASP.NET sample moves into the Playground - #177

Merged
MDA2AV merged 3 commits into
mainfrom
feat/nghttp2-streamed-response
Aug 10, 2026
Merged

nghttp2: streamed response bodies, and the ASP.NET sample moves into the Playground#177
MDA2AV merged 3 commits into
mainfrom
feat/nghttp2-streamed-response

Conversation

@MDA2AV

@MDA2AV MDA2AV commented Aug 10, 2026

Copy link
Copy Markdown
Owner

nghttp2 streams response bodies now. It always could - our shim was the part that couldn't.

The gap was ours, not nghttp2's

nghttp2_data_provider, NGHTTP2_ERR_DEFERRED and nghttp2_session_resume_data exist for exactly this. ioxide_nghttp2_shim.c only ever exposed the whole-body submit, so there was nothing for a writer to push into. That makes this mostly a C change.

Three new entry points:

ih2_submit_response_stream opens a stream with a provider and no bytes
ih2_stream_write appends a chunk and resumes the deferred stream
ih2_stream_close no more body; END_STREAM follows what is queued

The behaviour actually lives in read_body: on an empty window it now returns NGHTTP2_ERR_DEFERRED for a streamed body instead of flagging EOF. That is what holds a stream open through a slow producer rather than truncating it at its first quiet moment.

It is a PULL, and the C# hides that

Nghttp2ResponseWriter is an IBufferWriter<byte> and reads like a push, but underneath nghttp2 asks for bytes when it is ready to frame them. So a flush here means handed over, not on the wire - nghttp2 owns frame boundaries and timing. ioxide.http2 owns its own framing and stages a DATA frame the moment you flush; that difference is the whole reason this needed native work instead of a C# writer.

Verified on the wire

A byte count cannot tell streaming from one coalesced blob, so nghttp was the check:

recv HEADERS  (END_HEADERS, no END_STREAM)
recv DATA x8  (1024 each - one per flush)
recv DATA len=0 ; END_STREAM

/feed streams 251 MB in three seconds. The regression test counts frames, not bytes, for the same reason.

One bug worth recording because it will catch the next person: resume_data returns INVALID_ARGUMENT when the stream is not currently deferred, which happens whenever a write beats nghttp2 to it - most of them. Treating it as an error killed the connection after a single chunk: the finite case returned 0 bytes and /feed stopped dead at 1024.

Also here

Examples.AspNet was the last thing left at the repo root from the old Examples project. It moves to Playground/AspNet and is verified still serving on the io_uring transport.

0.4.177 across eleven packages. ioxide.nghttp2 is the one carrying a changed native library, and the packed nupkg was checked to contain the rebuilt .so exporting all three new symbols.

Not done

  • Request-body streaming. The shim already delivers chunks as they arrive (on_data_chunk_recv), so a reader is nearly all C# - but nghttp2 credits the flow-control window automatically, so it would be a reader with no backpressure. Real credit-as-you-read needs nghttp2_option_set_no_auto_window_update plus nghttp2_session_consume.
  • The .so is linux-x64 only, built from nghttp2 master rather than a pinned tag. Worth pinning before a release goes out on it.

Unit 36, Chaos 47, Http 38, E2E 46 - all 0 failed, 0 skipped.

MDA2AV added 3 commits August 10, 2026 20:26
Examples.AspNet was the last thing left at the repo root from the old Examples
project, which the Playground replaced. It is a sample like every other one, so
it lives with them: Playground/AspNet, Playground.AspNet, and the solution and
README point at the new path.

Nothing about what it does changes - still ioxide.Kestrel against a stock
Kestrel baseline, still switched by TRANSPORT. Verified by running it: / and
/plaintext both answer on the io_uring transport.
nghttp2 always supported this - it is what nghttp2_data_provider,
NGHTTP2_ERR_DEFERRED and nghttp2_session_resume_data exist for. What did not
support it was our shim, which only ever exposed the whole-body submit. So this
is mostly C.

Three new entry points: ih2_submit_response_stream opens a stream with a
provider and no bytes, ih2_stream_write appends and resumes, ih2_stream_close
says there will be no more. read_body is where the actual behaviour lives - on
an empty window it now returns NGHTTP2_ERR_DEFERRED for a streamed body instead
of flagging EOF, which is what holds the stream open through a slow producer
rather than truncating it at its first quiet moment.

The model is PULL, and the C# hides it: Nghttp2ResponseWriter is an
IBufferWriter<byte> that reads like a push, while underneath nghttp2 asks for
bytes when it is ready to frame them. A flush here means "handed over", not "on
the wire" - unlike ioxide.http2, which owns its framing and stages a DATA frame
the moment you flush. Request bodies are still buffered; that direction needs
manual window accounting to be worth anything, so it is not done here.

One bug worth recording: resume_data returns INVALID_ARGUMENT when the stream
is not currently deferred, which happens whenever a write beats nghttp2 to it -
most of them. Treating that as an error killed the connection after a single
chunk, which is exactly what the first run did: 8192 bytes came back as 0, and
/feed stopped at 1024.

Verified on the wire, because a length check would not have caught coalescing:
nghttp shows HEADERS, then EIGHT separate 1024-byte DATA frames, then a
zero-length DATA with END_STREAM. /feed streams 251 MB in three seconds. The
regression test counts frames rather than bytes for the same reason.

Unit 36, Chaos 47, Http 38, E2E 46.
Eleven packages this time - ioxide.nghttp2 is among them again, and it is the
one with something new in it: streamed response bodies, which means the shipped
native library changes as well as the managed assembly.

Verified by packing: the nupkg carries the rebuilt
runtimes/linux-x64/native/libioxide_nghttp2.so, and that .so exports
ih2_submit_response_stream, ih2_stream_write and ih2_stream_close.
@MDA2AV
MDA2AV merged commit af69e6d into main Aug 10, 2026
1 check passed
Sign up for free to 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.

1 participant