Repository navigation
HTTP/3: static delivery (addStaticHandler + $res->sendFile()) is not wired #60
Description
Activity
Investigation — the machinery exists, only the dispose hand-off is missing
The H3 static-file pump is fully implemented and registered, just never invoked:
h3_stream_send_static_response(http3_static_response.c:213) — coroutine pump that reads the file in 16 KiB slices and feeds the streaming path (h3_stream_append_chunk→ lazysubmit_response(streaming)→h3_stream_mark_ended), with acked-data/window backpressure.- It is wired into the vtable:
http3_callbacks.c:840.send_static_response = h3_stream_send_static_response. - It is reached via
http_send_file_dispatch()→ops->send_static_response.
The only gap:
h3_handler_coroutine_dispose(http3_dispatch.c) never checkshttp_response_has_send_file()and never callshttp_send_file_dispatch(). H2 does exactly this —http2_strategy.c:555-556→h2_sendfile_arm(:629) →http_send_file_dispatch(... h2_sendfile_on_done ...).The delicate part — async refcount hand-off
http3_stream_submit_responsereadsZ_OBJ(s->response_zv)live for:status/headers (http3_callbacks.c:506-511). The pump submits asynchronously (a separate enqueued coroutine, on the firstappend_chunk). So the dispose must not dropresponse_zv/ release the stream when it hands off —response_zvhas to survive until the pump's first submit, and the dispose tail (dropresponse_zv, drain-evaluate + GOAWAY,drain_out,http3_stream_release) must move into anon_donecallback that fires when the pump finishes. This mirrors H2'sh2_sendfile_on_donedeferring the response-zval dtor.Implementation plan
- In
h3_handler_coroutine_dispose, in the buffered branch:if (http_response_has_send_file(Z_OBJ(s->response_zv)))→ takehttp_response_take_send_file(),http_send_file_dispatch(s->request, Z_OBJ(s->response_zv), sf_req, h3_sendfile_on_done, user); on success return early (skip the response_zv drop + stream_release). - Add
h3_sendfile_on_done(user, status)doing the deferred tail: dropresponse_zv, drain-evaluate/GOAWAY,drain_out+arm_timer,http3_stream_release(s). Dispatch-failure path (returns false → response carries a synthesized 500) falls through to the normal buffered submit. - (Optional, second commit) mount routing: call
http_static_try_servein the H3 dispatch like H2http2_strategy.c:264, foraddStaticHandler. - Phase-5 copy-elim (the
emalloc(16K scratch)+zend_string_initper chunk inhttp3_static_response.c) is done inside this work, now that it is testable.
Tests
- New phpt:
$res->sendFile()over h3client across the 16 KiB chunk boundary (e.g. ~40 KiB file → 3 slices), plus HEAD, 404/path-traversal, and a dispatch-failure (→ 500) case. - Leak/UAF check: sustained c=64 sendFile stress (the regime that surfaced the dirty-list UAF).
Risk note: the response_zv / stream refcount across the async boundary is the same UAF class as #62 — implement and test carefully, not in a rush.
- added a commit that references this issue
on May 31, 2026 - added a commit that references this issue
on Jun 15, 2026
Problem
Static file delivery over HTTP/3 does not work.
addStaticHandler(...)(mount routing) and\$res->sendFile(...)over an H3 connection return200with an empty body and no FIN — identical to a baseline with no static support.Root cause
H1 and H2 both wire static delivery into their request paths:
src/core/http_connection.c→http_static_try_serve(mount routing) +http_send_file_dispatch(sendFile in dispose).src/http2/http2_strategy.c→ same two calls.src/http3/calls neitherhttp_static_try_servenorhttp_send_file_dispatch. Consequently the H3 static pump coroutine (http3_static_response.c/h3_static_pump_entry) is dead code — it never runs (verified: empty trace on asendFilerequest over H3).Fix direction
http_send_file_dispatch(mirror H2http2_strategy.c), and optionallyhttp_static_try_servein dispatch for mount routing.emalloc(16K scratch)+zend_string_initdouble-copy inhttp3_static_response.c, ~:124/:173) should be done inside this wiring work — it only becomes testable once static actually flows (asendFilephpt with the h3 client across the 16K chunk boundary).Context
Split out of #59 (HTTP/3 staged performance roadmap), where it was the Phase-5 finding that got deferred.