Skip to content

fix: rewrite static asset middleware and filesystem provider - #29

Merged
oboard merged 2 commits into
mainfrom
worktree-fix-static-assets-issue
Aug 21, 2026
Merged

fix: rewrite static asset middleware and filesystem provider#29
oboard merged 2 commits into
mainfrom
worktree-fix-static-assets-issue

Conversation

@oboard

@oboard oboard commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes the static asset middleware issues reported against main@c545db2: unrelated routes dying on an out-of-range slice (PanicError killing the process), directory indexes never served, missing Content-Type, a filesystem provider that returned Some metadata for every candidate without checking the filesystem, synchronous moonbitlang/x/fs I/O inside async request handlers, and all I/O errors collapsing to 404.

Changes

Middleware (static.mbt)

  • Mount matching is now a path-prefix check on a segment boundary (/assets matches /assets and /assets/..., not /assetsx), and the URL is only sliced after that relationship is validated — shorter, unrelated URLs fall through to the router untouched instead of panicking.
  • Asset ids are normalized under a virtual root, so .. segments can never escape the configured root (the provider only ever sees confined, absolute virtual ids).
  • Index probing joins paths correctly (/sub + index.html/sub/index.html) and no longer mutates the provider's index-name array.
  • Content-Length is emitted for empty files too.

Provider contract (ServeStaticProvider)

  • get_meta/get_contents are now async; the contract documents that get_meta must return None only for absent/unservable candidates and raise real I/O failures (surfaced as 500, not a misleading 404).

Filesystem provider (static_file)

  • get_meta performs real filesystem checks and reports actual size/mtime/type metadata; directories are skipped so index probing continues.
  • Built-in MIME table (mime_type_of) — responses carry a real Content-Type, plus Content-Length and a weak ETag (If-None-Match → 304).
  • Native backend does all file I/O through moonbitlang/async/fs (new internal nativefs helper package) instead of blocking the event loop. The JS backend keeps moonbitlang/x/fs — no async fs exists for JS yet — with the blocking behavior and missing size/mtime metadata documented.
  • Read errors distinguish a vanished file (404) from other I/O failures (500).
  • New optional fallthrough / index_names configuration; examples/static_assets (mounted at /) opts into fallthrough=true so its explicit routes keep working.

Tests

  • static_wbtest.mbt: middleware-level tests with an in-memory provider — mounted paths, unrelated/short routes, segment boundary, indexes, MIME, HEAD, ETag/304, fallthrough, root confinement.
  • static_file/static_file_blackbox_test.mbt (+ native-only static_file_metadata_test.mbt): black-box tests over a real fixture tree through dispatch_http — the issue's full reproduction table.

Verification

  • moon test --target native: 75/75 pass; moon test --target js: 80/80 pass; moon check clean on both targets.
  • Ran the issue's exact reproduction against a live native server:
Request Before After
GET /assets 404 200 index fixture
GET /assets/ 404 200 index fixture
GET /assets/app.txt 200, no Content-Type 200, text/plain, Content-Length, ETag
GET /api PanicError, process exit 200 api ok
GET /a (process already dead) 404, server alive
GET /assets/../secret.txt 404 (confined)
POST /assets/app.txt 405 Allow: GET, HEAD
If-None-Match 304

🤖 Generated with Claude Code

Address the static asset middleware issues reported against main@c545db2:

- Match the mount as a path prefix on a segment boundary and never slice
  the URL before the mount relationship is validated, so unrelated or
  shorter routes (e.g. GET /api, GET /a) fall through instead of dying
  on an out-of-range slice that aborted the process.
- Resolve asset ids under a virtual root so ".." segments are normalized
  away and can never escape the configured root.
- Probe index files with correct path joining, so the mount root and
  directories serve their index.html (a trailing "/" no longer 404s).
- get_meta now performs real filesystem checks and reports actual
  size/mtime/type metadata instead of returning Some for every candidate;
  asset ids that are directories are skipped so probing continues.
- Built-in MIME table: responses now carry a real Content-Type, plus
  Content-Length and a weak ETag (If-None-Match yields 304).
- Native backend uses moonbitlang/async/fs for all file I/O instead of
  blocking synchronous calls inside async request handlers; the JS
  backend keeps moonbitlang/x/fs (no async fs exists for JS yet) and
  documents the blocking behavior and missing metadata.
- Distinguish not-found from other I/O failures: ENOENT/ENOTDIR map to
  404 (or fallthrough), anything else surfaces as 500.
- Preserve fallthrough: providers can opt in, letting later routes and
  other methods through; the "/" example uses it to keep its routes.
- ServeStaticProvider::get_meta/get_contents are now async; the provider
  gains optional fallthrough/index_names configuration.

Adds black-box tests driving dispatch_http end to end: mounted paths,
unrelated and short routes, directory indexes, MIME types, HEAD,
conditional requests, fallthrough, and root confinement. Verified live
against a native server with the issue's original reproduction table.

Co-Authored-By: Claude <noreply@anthropic.com>
@oboard
oboard force-pushed the worktree-fix-static-assets-issue branch from 3c55b4f to 392d78f Compare August 21, 2026 09:55
Replace the hand-rolled MIME table in the static file provider with the
oboard/mimetype@0.2.0 database. mime_type_of keeps its signature and
still appends charset=utf-8 for text/* types, so served Content-Type
values are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
@oboard
oboard merged commit a982650 into main Aug 21, 2026
1 check passed
@oboard oboard mentioned this pull request Aug 21, 2026
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