Repository navigation
fix(pages): keep a static file out of a loader not-found response - #822
Merged
cpojer merged 2 commits intoNov 13, 2026
Merged
Conversation
Hono resolves `c.notFound()` inside the loader call, so the loader returns whatever the app fallback produced. When a static file sat at the same path, the asset fallback returned its bytes and `runLoader` read a 200 JSON response as page props, rendering the file as the page's data. Mark the request when a page loader calls `c.notFound()`, and let the fallback skip asset lookup for it. The marker outlives the loader because the fallback answers inside the call. A matching page route now owns the URL once its loader answers 404.
Collaborator
Author
|
👁️🗨️ Review this pull request with grouped, summarized diffs at: raw result💭 analyzed by {
"headSha": "c0dec0124672dea884bd86c9a11ce424da9804f1",
"result": {
"overallSummary": "Fixes #776. When a page loader called `c.notFound()`, Hono resolved the not-found inside the loader, and if a static asset existed at the same path the fallback served it; `runLoader` then read that 200 response as page props and the page rendered the file's data instead of a 404.\n\nThe loader's `c.notFound()` now records a per-request decision, the loader is always wrapped (not just when the app has its own boundary), and the fallback treats the URL as claimed: it skips static assets and answers 404 without rendering the page. Apps with their own not-found boundary keep their current behaviour.",
"groups": [
{
"key": "loader-not-found-decision",
"label": "Loader not-found decision",
"summary": "Replaces the ephemeral `WeakSet` boundary marker with a per-request `WeakMap` decision (`boundary` vs `fallback`) set by an instrumented `c.notFound()`, so the loader's choice outlives the loader call and is visible to the fallback.\n\n`runLoader` always wraps the handler now — previously the wrapper was skipped when the app had no boundary — and the fallback splits the old single check into \"app boundary owns it, return bare 404\" and \"loader claimed the URL, do not serve a static asset\".",
"category": "core",
"filePaths": [
"packages/void/src/pages/not-found-context.ts",
"packages/void/src/pages/protocol.ts",
"packages/void/src/runtime/routing-pages.ts"
],
"lineNotes": [
{
"path": "packages/void/src/pages/not-found-context.ts",
"side": "additions",
"line": 37,
"text": "Only the first `c.notFound()` of a request is recorded, so an outer page's decision wins when a fallback render re-enters another loader. The decision is deliberately never cleared: only `c.notFound` is restored in `finally`, because the fallback reads the marker after the loader returns."
},
{
"path": "packages/void/src/runtime/routing-pages.ts",
"side": "additions",
"line": 204,
"text": "This branch returns a plain 404 instead of going through `options.routing.markNoMatch(...)` like the other no-asset path — worth confirming routing bookkeeping/caching does not depend on that call for loader-claimed URLs."
}
]
},
{
"key": "not-found-asset-test",
"label": "Not-found asset test",
"summary": "New case in the existing `loader not-found dispatch` suite serves a JSON asset at the requested path and asserts a 404, that the asset was never fetched, and that no page render happened — the exact regression from #776.",
"category": "tests",
"filePaths": [
"packages/void/test/unit/page-boundaries.test.ts"
],
"fileNotes": [
{
"path": "packages/void/test/unit/page-boundaries.test.ts",
"text": "The `fetched` assertion is the meaningful one: it pins the \"asset not read\" half of the fix, while the status/text checks would also pass if the asset were fetched and then discarded."
}
]
}
],
"schemaVersion": 1,
"source": "llm",
"generatedAt": "2026-11-13T22:13:54.716Z",
"model": "openai-compatible/opencode-go/deepseek-v4.1-flash",
"locale": "en"
}
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #776
Description
A page loader that calls
c.notFound()returned the app fallback's response, because Hono resolvesc.notFound()inside the loader call. When a static file sat at the same path, the fallback served it,runLoaderread the 200 JSON as page props, and the page rendered the file's data instead of a 404.The request now records the loader's decision, and the fallback skips static assets for it. A matched page route owns its URL once the loader answers 404, which matches the rule this fallback already applied to root rendering. Apps with their own not-found boundary keep their current behaviour.
Test
packages/void/test/unit/page-boundaries.test.ts— new case serves a JSON asset at the requested path and asserts a 404 with no asset read and no page render. It fails onmainwithexpected 200 to be 404.Also built
playground/vue-pageswith apublic/docs/readme.jsonand a/docs/[name]page whose loader returnsc.notFound():/docs/readme.json→200 text/html, page props{"static":"docs"}, server loggedTypeError: Cannot read properties of undefined (reading 'title')/docs/readme.json→404, the same as any other missing asset path