Skip to content

Built-in static file handler #13

Description

@EdmondDantes

Tracks FUTURES.md item #2. Unblocks the production tier for HttpArena static profiles (static, static-h2, static-h3) — the only remaining blocker before meta.json can flip from "type": "tuned" to "type": "production".

Full design + implementation plan: docs/PLAN_STATIC_HANDLER.md.

Why

Today entry.php slurps /data/static into a userland hash table and hand-rolls .br/.gz selection. Production rules forbid user-land MIME lookup, in-memory caching, and hand-rolled compression — so the static profiles cannot be marked production until the framework provides a native primitive.

HttpServer / HttpServerConfig have no static-file API; this issue introduces one.

Surface (PHP)

final class TrueAsync\StaticHandler {
    public function __construct(string $urlPrefix, string $rootDirectory);
    public function setIndexFiles(string ...$files): static;
    public function setOnMissing(StaticOnMissing $mode): static;        // NotFound | Next
    public function enablePrecompressed(string ...$encodings): static;  // 'br' | 'gzip' | 'zstd'
    public function setDotfilePolicy(StaticDotfiles $p): static;        // Deny (default) | Allow | Ignore
    public function setSymlinkPolicy(StaticSymlinks $p): static;        // Reject (default) | Follow | OwnerMatch
    public function hide(string ...$globs): static;
    public function setEtagEnabled(bool $enabled): static;              // weak ETag, default true
    public function setCacheControl(string $value): static;
    public function setHeader(string $name, string $value): static;     // declarative, no callbacks
    public function setBrowseEnabled(bool $enabled): static;            // default false
    public function setMimeType(string $extension, string $contentType): static;
}

$server->addStaticHandler(StaticHandler $h): static;

Builder class (not array $options) — symmetric to HttpServerConfig, gives IDE autocomplete and per-setter validation. No PHP callback per request — that re-enters the VM and kills the zero-coroutine fast path.

Architecture (key decision)

Dispatch happens in C, before ZEND_ASYNC_NEW_COROUTINE at src/core/http_connection.c:1614. The static path is a pure FSM driven by uv_fs_open → fstat → read-loop → close. No coroutine alloc, no zend_try, no PHP-VM entry on matched static requests.

if (UNEXPECTED(server->static_handler_count > 0)) {
    http_static_result_t rc = http_static_try_serve(server, conn, ctx, req);
    if (rc == HTTP_STATIC_HANDLED) return;        // C owns lifecycle
    if (rc == HTTP_STATIC_ERROR)   { emit_short_error(...); return; }
    /* PASSTHROUGH falls through to coroutine + PHP handler */
}

A shared http_request_finalize(conn, ctx) helper, extracted from http_handler_coroutine_dispose, runs from both the coroutine path and the static FSM tail.

For HTTP/2 / HTTP/3 the file is plumbed as an nghttp2 / nghttp3 data provider — natural fit, still zero coroutine.

Hot-path budget

Documented in detail in §4 of the plan. Highlights:

  • One open(O_RDONLY|O_CLOEXEC) + one fstat per request. No probe-stat.
  • 64 KiB read buffer, single emalloc per request, freed in close-callback.
  • Built-in MIME table (perfect-hash / sorted binary search), per-mount overrides only on miss.
  • Weak ETag = mtime_ns ^ (size << 17) ^ ino, single 64-bit mix, 16 hex chars.
  • EXPECTED / UNEXPECTED on dispatcher and FSM branches.
  • All per-mount config validated and materialized at addStaticHandler time, not per request.
  • writev-coalesced headers + first read chunk for small files (H1 plain).

PR breakdown

PR Scope Blocks
#1 StaticHandler class, dispatch hook (H1 only), MIME, traversal guard, weak ETag, conditional GET, uv_fs_* chain, http_request_finalize extraction. PHPT matrix: 200 / 304 / 404 / 403 / traversal / dotfile-deny. —
#2 H2/H3 integration via nghttp2/nghttp3 data providers. PHPT matrix on H2 + H3. #1
#3 Range support (single, suffix, multipart/byteranges, If-Range, 416). #1
#4 Precompressed sidecars .br / .gz / .zst. Reuses compression/http_compression_negotiate.c. #1, #3
#5 sendfile(2) / splice(2) zero-copy fast path (H1 plain, no on-the-fly encoding). #1
#6 Optional browse (dir listing). Lowest priority. #1

After PR #1+#2+#3+#4 land: rewrite entry.php to drop the in-memory map and the hand-rolled encoding chooser, then flip frameworks/true-async-server/meta.json to "type": "production" for the static profiles. Closes FUTURES #2.

Acceptance

  • StaticHandler class compiles, stubs regenerate, arginfo hash matches.
  • addStaticHandler rejects calls after start().
  • enablePrecompressed('foo') throws at the setter, not at start().
  • Path traversal (.., %2e%2e, %00, NUL, absolute paths) → 400/404.
  • Symlink policy: Reject 404s, Follow serves, OwnerMatch serves only when owner-of-link == owner-of-target.
  • If-None-Match / If-Modified-Since → 304 with empty body.
  • Dotfile policy default Deny → 404 on /.git/config.
  • on_missing: Next falls through to addHttpHandler; default NotFound returns 404 in C.
  • Telemetry confirms zero coroutines spawned on matched static requests.
  • wrk -c 256 -t 4 -d 30 /static/main.css does not regress vs current entry.php; expected 5–15% gain from the coroutine skip.
  • No new leaks under valgrind on a million-request run.

Open questions parked

  1. Per-worker FD/stat cache (open_file_cache-style) — defer until bench shows it matters.
  2. Per-listener static handler scoping (admin port wants no static) — currently global, additive option later.
  3. browse HTML dir listing — PR Support running on stock PHP (without TrueAsync fork) #6, low priority.

Anchors

  • Insertion site: src/core/http_connection.c:1562 (http_connection_dispatch_request).
  • Coroutine spawn to skip: src/core/http_connection.c:1614.
  • Compression hook order: src/core/http_connection.c:1601.
  • H2 dispatch: src/http2/http2_strategy.c:156.
  • H3 dispatch: src/http3/http3_dispatch.c:134.
  • Handler registration template: src/http_server_class.c:944-966 (addHttpHandler).
  • Config-class template: src/http_server_config.c.

Activity

  1. moved this to In Progress in True Async Boardon May 6, 2026
  2. added this to the Next milestone on May 6, 2026
  3. self-assigned this
    on May 6, 2026
  4. added 20 commits that reference this issue on May 7, 2026
  5. linked a pull request that will close this issue13 built in static file handler #14on May 11, 2026
  6. moved this from In Progress to Done in True Async Boardon May 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions