Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
3a51405
feat(static): scaffold StaticHandler class + addStaticHandler (issue …
EdmondDantes May 6, 2026
143f491
feat(static): MIME table, path resolution, weak ETag (issue #13)
EdmondDantes May 7, 2026
bfe0e7b
feat(static): wire dispatch FSM, end-to-end serve (issue #13)
EdmondDantes May 7, 2026
d7e84c7
refactor(static): comment cleanup, const correctness, hot-path hints
EdmondDantes May 7, 2026
1eae0d4
fix(static): close symlink-traversal, backslash, info-disclosure gaps
EdmondDantes May 7, 2026
1ad7724
feat(io): wire ZEND_ASYNC_IO_ZERO_COPY_OK on conn->io (zend_async API…
EdmondDantes May 7, 2026
25e0cff
refactor(connection): extract http_request_finalize from coroutine di…
EdmondDantes May 7, 2026
e780ca2
revert(io): drop ZERO_COPY_OK on conn->io (flag removed from API)
EdmondDantes May 7, 2026
04fbc6f
feat(static): hard-zero coroutine path via async open + sendfile
EdmondDantes May 7, 2026
5b71c5f
docs(static): update PLAN_STATIC_HANDLER status section
EdmondDantes May 7, 2026
6e3244c
fix(static): refcounted shared mounts survive worker-pool fan-out
EdmondDantes May 8, 2026
67eadb9
fix(static): TCP_CORK headers+sendfile coalescing
EdmondDantes May 8, 2026
1a68003
perf(static): pre-bake extra_headers + Cache-Control at freeze time
EdmondDantes May 8, 2026
9894a18
perf(static): index-walk via sync stat + async hard-zero open (#5b)
EdmondDantes May 8, 2026
d8efcbb
feat(static): on_missing:Next rollback from hard-zero (#5c)
EdmondDantes May 8, 2026
01d2ca3
chore(static): yellow cleanups (#8/#9/#12/#13/#15)
EdmondDantes May 8, 2026
563df27
chore(static): dedupe status-line table (#10) + document dir→404 (#14)
EdmondDantes May 8, 2026
2c46937
test(static): valgrind close-out + IMS coverage + hard-zero counter
EdmondDantes May 8, 2026
8e4b0fc
docs(static): sync §5a status to reality + flag kTLS BIO blocker
EdmondDantes May 8, 2026
b869903
feat(static): TLS hard-zero via chunked SSL_write FSM (#13 §5a)
EdmondDantes May 8, 2026
81bc752
feat(static): open file cache (HashTable + LRU + TTL, issue #13 §5a)
EdmondDantes May 8, 2026
0aab165
feat(static): cache stores real metadata + hit/miss counters (#13 §5a)
EdmondDantes May 8, 2026
35e4963
perf(static): hand-format ETag + IMF date, drop snprintf from hot path
EdmondDantes May 8, 2026
8f05e8d
feat(static): per-handler open-file cache config (#13 §5a)
EdmondDantes May 8, 2026
e328522
perf(static): hit-side bypass — skip IO_STAT/etag/MIME on cache hit (…
EdmondDantes May 8, 2026
7302ea7
feat(static): real Symlink::OwnerMatch + REJECT final-segment guard (…
EdmondDantes May 8, 2026
eb18e1f
feat(static): precompressed sidecars (.gz/.br/.zst) per Accept-Encodi…
EdmondDantes May 8, 2026
6c99358
feat(static): single byte-range + If-Range (#13 PR #3)
EdmondDantes May 8, 2026
9513c6c
docs(static): sync §5a + §6 + §7 to closed work in this branch
EdmondDantes May 8, 2026
c131c8b
chore(static): clang-format pass + naming + decoder tests + TOCTOU re…
EdmondDantes May 8, 2026
aaa9a73
style(static): blank lines between logical blocks (§13a.1) + const pa…
EdmondDantes May 8, 2026
66a2e2a
refactor(static): extract sendfile into protocol-owned vtable op
EdmondDantes May 8, 2026
49a19bd
refactor(static): protocol-agnostic try_serve dispatch
EdmondDantes May 8, 2026
5b38f17
refactor(static): code-review cleanup — dead state, dead prebake, dup…
EdmondDantes May 8, 2026
c9a22c6
perf(static): pre-allocate head smart_str to skip realloc rounds
EdmondDantes May 8, 2026
2d25ab5
perf+cleanup(static): hot-path optimizations and self-review fixes
EdmondDantes May 8, 2026
ab1b281
docs(static): trim session-narration comments, dedup with code
EdmondDantes May 8, 2026
24bdecd
refactor(static): cosmetic cleanup — names, internal header, phase enum
EdmondDantes May 8, 2026
cd3c6ec
feat(static): HTTP/2 send_static_response — protocol-owned static del…
EdmondDantes May 8, 2026
6a34c52
refactor: dedup HTTP/1 head serializer + strip dead compression ifdefs
EdmondDantes May 9, 2026
cb9e16a
style: add const to read-only scalar params/locals in branch hot paths
EdmondDantes May 9, 2026
910d26d
feat(response): HttpResponse::sendFile + SendFileOptions value-object…
EdmondDantes May 9, 2026
699fe9f
refactor: lift request/response helpers to public API, drop local dupes
EdmondDantes May 9, 2026
b1020c8
docs(arch): add http_rfc5987 to step 2; add deduplication backlog sec…
EdmondDantes May 9, 2026
0c0f4d4
refactor(file-delivery): extract shared HTTP utilities (Шаг 2)
EdmondDantes May 9, 2026
c638aa8
feat(send_file): single file-delivery engine (Шаг 3)
EdmondDantes May 9, 2026
fe7e4ba
refactor(static): drive StaticHandler via send_file engine (Шаг 4)
EdmondDantes May 9, 2026
df54b96
chore: drop accidentally-committed phpize backups; ignore *~
EdmondDantes May 9, 2026
0bb5b74
refactor(send_file): drive Response::sendFile via engine (Шаг 5)
EdmondDantes May 9, 2026
0c6c55d
refactor(static): collapse dispatch cbs into send_file_cbs_t (Шаг 6)
EdmondDantes May 9, 2026
a2e3a2c
docs(arch): finalize ARCH_FILE_DELIVERY.md after refactor completion
EdmondDantes May 9, 2026
1d161c7
style: drop redundant `*const` qualifiers; blank-line separate cleanu…
EdmondDantes May 9, 2026
c19138f
style: ensure blank line before `if (` and after closing `}`
EdmondDantes May 9, 2026
ed729e7
refactor(precompressed): extract sidecar resolver into shared TU
EdmondDantes May 9, 2026
aa138c4
refactor(multipart): unify Content-{Type,Disposition} param scanning
EdmondDantes May 9, 2026
cd51143
refactor: unify H2/H3 forbidden response-header filter
EdmondDantes May 9, 2026
e31f764
refactor(static): extract path-safety cluster into its own TU
EdmondDantes May 9, 2026
4b4cf6b
refactor: hoist extra-headers loop into http_response API
EdmondDantes May 9, 2026
0e742d5
feat(static): async file delivery on h2 + h3
EdmondDantes May 9, 2026
cd72a95
fix(static): gate async dispatch on req->completed
EdmondDantes May 9, 2026
ab317db
chore(bench): rename stale http_server.so EXT path to true_async_serv…
EdmondDantes May 9, 2026
23a8dbb
test(bench): add static-file delivery benchmark
EdmondDantes May 9, 2026
01ef843
fix(static): close fs_open fd on dispose to plug per-request fd leak
EdmondDantes May 9, 2026
62413e1
test(bench): enable open-file cache in static bench by default
EdmondDantes May 9, 2026
e10296b
Revert "fix(static): close fs_open fd on dispose to plug per-request …
EdmondDantes May 9, 2026
cd99d9e
perf(send_file): inline open(2)/fstat(2), drop thread-pool dispatch
EdmondDantes May 11, 2026
ed2d269
perf(send_file): small-file fast path — slurp + inline body, bypass s…
EdmondDantes May 11, 2026
e40eefa
fix(http2): seek to body_offset before first range read
EdmondDantes May 11, 2026
19758f7
style(send_file,http2): trim verbose comments
EdmondDantes May 11, 2026
789dae8
docs: changelog + ARCH update for static-delivery perf work
EdmondDantes May 11, 2026
1541f47
#13: Update docs
EdmondDantes May 11, 2026
fdc66f0
Merge remote-tracking branch 'origin/13-built-in-static-file-handler'…
EdmondDantes May 11, 2026
c981936
refactor: fold h2/h3 forbidden-header filter into http_response
EdmondDantes May 11, 2026
ec7cfde
Merge branch '13-built-in-static-file-handler' of https://github.com/…
EdmondDantes May 11, 2026
6114450
fix(fuzz): link http_param_parse + http_rfc5987 into harnesses
EdmondDantes May 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .clang-format
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
InsertBraces: true
AlwaysBreakAfterReturnType: None
BreakBeforeBinaryOperators: None
BreakBeforeTernaryOperators: true
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,4 @@ fuzz/crash-*
fuzz/leak-*
fuzz/timeout-*
fuzz/oom-*
*~
62 changes: 62 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,70 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `HttpResponse::sendFile(string $path, ?SendFileOptions $options = null): void`
— handler-driven file delivery. Records path + options on the response
and returns immediately; the protocol's `send_static_response` op
runs the actual transfer in the dispose phase, reusing the static
module's open-stat-sendfile FSM (MIME detection, ETag, IMF date,
Range, conditional GET, precompressed sidecars). Path is treated as
trusted (handler made the access decision). Open / fstat errors
surface as a 500 since headers aren't on the wire yet.
After `sendFile()` the response is sealed: `setHeader` / `setStatus*` /
`write` / `send` / `setBody` / `json` / `html` / `redirect` / `end` /
a second `sendFile()` throw `HttpServerRuntimeException`.
New value-object `TrueAsync\SendFileOptions` (`final readonly class`,
named-args constructor) carries `contentType`, `disposition`
(`SendFileDisposition::INLINE | ATTACHMENT`), `downloadName`,
`cacheControl`, `etag`, `lastModified`, `acceptRanges`,
`precompressed`, `conditional`, `deleteAfterSend`, `status` overrides.
Compression middleware is bypassed for sendFile bodies (own
delivery pipeline). HTTP/3 path is follow-up — the dispose hook
refuses with 500 for now.

### Changed

- Static-handler PHP enum cases renamed to UPPER_CASE for project-wide
consistency: `StaticOnMissing::{NotFound→NOT_FOUND, Next→NEXT}`,
`StaticDotfiles::{Deny→DENY, Allow→ALLOW, Ignore→IGNORE}`,
`StaticSymlinks::{Reject→REJECT, Follow→FOLLOW, OwnerMatch→OWNER_MATCH}`.
Breaking for any existing user code that referenced the old casings.

### Performance

- Static file delivery — inline `open(2)` / `fstat(2)` in `send_file`
engine (issue #13). The previous `ZEND_ASYNC_FS_OPEN` /
`ZEND_ASYNC_IO_STAT` chain routed both syscalls through the libuv
thread pool: one futex round-trip per request just to learn whether
a file existed. On a warm dentry cache both syscalls are sub-µs;
the dispatch was pure overhead. A 0-ms timer defers `engine_handle_stat`
off the synchronous tail so on_done cannot re-enter the request
dispatcher on its own call stack. Wins: H1 tiny 256B 19k → 35k req/s,
H1 304 If-None-Match 24k → 123k req/s.
- Static file delivery — small-file fast path (≤ 64 KiB). libuv's
`uv_fs_sendfile` on Linux is doubly broken for file→TCP-socket: it
tries `copy_file_range` first (returns EINVAL on socket), then falls
into a userspace `pread` + `write` loop inside a worker thread — no
kernel zero-copy plus a futex round-trip per request. Files at or
under 64 KiB are now slurped synchronously into a `zend_string` and
emitted as one `writev(headers + body)` through libuv's per-socket
write queue; ordering with prior writes is then libuv's problem.
Files above 64 KiB stay on the (broken-but-functional) sendfile
path. Wins on top of the inline-syscall change: H1 tiny → 103k req/s
(×2.9), H1 small 16K → 73k (×1.9), H2 tiny → 154k (×4.4), H2 small
→ 73k (×2.7).

### Fixed

- HTTP/2 single byte-range delivery served bytes from offset 0 of the
file regardless of the requested range start. The H2 body FSM stored
`body_offset` in its state but never applied it to the file: the
buffered read path (`ZEND_ASYNC_IO_READ`) uses the fd's tracked
position, which is 0 right after open. Seek the io once before the
first read when `body_offset > 0`. H1 was unaffected (sendfile path
passes the offset explicitly to the syscall).
Closes pre-existing failure of `tests/phpt/server/static/012-static-h2`.
- Proactive drain mis-fired on the first response when CoDel/telemetry
were disabled. The fallback timestamp used `CLOCK_MONOTONIC_COARSE`
while `created_at_ns` and `drain_not_before_ns` are `zend_hrtime`
Expand Down
21 changes: 21 additions & 0 deletions config.m4
Original file line number Diff line number Diff line change
Expand Up @@ -464,16 +464,34 @@ if test "$PHP_HTTP_SERVER" != "no"; then
src/core/conn_arena.c
src/http1/http_parser.c
src/http1/http1_stream.c
src/http1/http1_sendfile.c
src/formats/multipart_parser.c
src/formats/multipart_processor.c
src/http_request.c
src/http_response.c
src/http_mime.c
src/http_date.c
src/http_etag.c
src/http_conditional.c
src/http_range.c
src/fs_util.c
src/http_rfc5987.c
src/http_precompressed.c
src/http_param_parse.c
src/send_file.c
src/http_send_file.c
src/http_send_file_options.c
src/uploaded_file.c
src/core/http_protocol_handlers.c
src/core/http_protocol_strategy.c
src/core/http_known_strings.c
src/log/http_log.c
src/log/trace_context.c
src/static/static_handler_class.c
src/static/http_static_path.c
src/static/http_static_cache.c
src/static/http_static.c
src/static/http_static_safety.c
"

dnl TLS-only TU split out of http_connection.c (BIO ring writer +
Expand All @@ -491,6 +509,7 @@ if test "$PHP_HTTP_SERVER" != "no"; then
src/http2/http2_strategy.c
src/http2/http2_session.c
src/http2/http2_stream.c
src/http2/http2_static_response.c
"
fi

Expand Down Expand Up @@ -532,6 +551,7 @@ if test "$PHP_HTTP_SERVER" != "no"; then
src/http3/http3_io.c
src/http3/http3_callbacks.c
src/http3/http3_dispatch.c
src/http3/http3_static_response.c
src/http3/http3_stream.c
src/http3/http3_stream_pool.c
"
Expand Down Expand Up @@ -572,6 +592,7 @@ if test "$PHP_HTTP_SERVER" != "no"; then
PHP_ADD_BUILD_DIR([$ext_builddir/src/http1])
PHP_ADD_BUILD_DIR([$ext_builddir/src/formats])
PHP_ADD_BUILD_DIR([$ext_builddir/src/log])
PHP_ADD_BUILD_DIR([$ext_builddir/src/static])
PHP_ADD_BUILD_DIR([$ext_builddir/deps/llhttp])

if test "$PHP_HTTP2" = "yes"; then
Expand Down
Loading
Loading