You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 9f67470
Browse filesBrowse the repository at this point in the historyBrowse files
fix(pages): keep the named action key out of middleware redirects, rewrites and the page URL (#530)
Follow-ups to #522. That PR dropped the action key (`?delete`) only from
`c.redirect()` inside the action. This covers the other places it
leaked.
## What changes
| Case: `POST /books?delete&page=2` | Before | After |
|---|---|---|
| Auth / user middleware `c.redirect('/login')` | `/login?delete&page=2`
| `/login?page=2` |
| Action `c.rewrite('/api/x')`, then `c.redirect('/done')` |
`/done?delete&page=2` | `/done?page=2` |
| `c.redirect('/books-v2', 307)` (or 308) | `/books-v2?page=2` (since
#522) | `/books-v2?delete&page=2` |
| `PUT /books?update` + `c.redirect('/books')` (302) | `/books` (since
#522) | `/books?update` |
| API route under a catch-all named-action page: `POST /api/x?return=/a`
+ redirect | `?return` dropped (earlier commit here) | `?return=/a` kept
|
| Action re-renders the page: `page.url` | `/books?delete&page=2` |
`/books?page=2` |
| …address bar after `useForm` / `action()` | `/books?delete&page=2` |
`/books?page=2` |
The key goes only when the browser follows with a GET (303, or 301/302
after a POST). Other redirects send the same method again, and the
destination needs the key to pick the action.
Redirect and rewrite **rules** (`void.json` routing) are unchanged: they
keep the query as sent. On Void Cloud, platform dispatch applies them
before the Worker and knows nothing about pages, so every target leaves
them alone.
## How
```
compile.ts app.use(createNamedActionMarker(matchedRoutes)) ← after routing rules, before auth and user middleware
matchedRoutes comes from the app's own resolved hono/route (Hono keys it by a per-copy Symbol)
marks only if the first matched Void route handler (tagged: API, page GET, page mutation)
is a named-action page; user middleware of any arity is skipped
routing.ts c.redirect: drop the first query segment if marked and the browser follows with a GET
c.rewrite: no new query → mark the new request too
protocol.ts mutation page.url: drop the key if marked
client after a POST that renders: if page.url's query == our URL minus its first segment,
write that to history
pages/action-key.ts shared WeakSet + string helper (no regex). Rolldown drops the WeakSet from client bundles.
```
Docs: the actions guide now says what happens to the query. The rewrites
reference mentions middleware and 307/308.
## Flaky tests (second commit)
Each of these failed in a local full run under load:
| Test | Cause | Fix |
|---|---|---|
| `plugin-class-a-wrapper` `… during build` | macOS reports
`server.mjs`, written just before startup, as changed (2 of 6 runs in a
probe). The reload gives the second import a new `Counter`. | `watch:
null` (12 of 12 runs stable) |
| `plugin-vite-environment` #472 test | Real dev server: about 1 s
alone, >5 s under load | `watch: null`, lighter schema file, 30 s limit
|
| `plugin-entry-imports` | Imports the whole plugin entry: about 4.2 s
alone | 30 s limit |
## Tests
- `routing-runtime.test.ts`: middleware, 307, rewrite, rewrite with its
own query, and page URL. 5 rows fail on main.
- `pages-client.test.ts`: history URL after a POST, 4 rows. 2 fail on
main; the other 2 check that single-action URLs stay unchanged.
- `pages-compile.test.ts`: the marker is emitted after
`routingMiddleware` and before user middleware, only for `actions`.
- `vp run test --unit`: all pass, except one `cf-version-runner` test
that timed out under load and passes alone (not touched here). Lint,
format and typecheck are clean.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/guide/pages-routing/actions-and-forms.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -197,7 +197,7 @@ export const actions = {
197
197
};
198
198
```
199
199
200
-
Named actions are dispatched via a `?actionName` suffix on the URL (e.g. `/users/42?update`). The client primitives `useForm` and `action()` handle this automatically.
200
+
Named actions are dispatched via a `?actionName` suffix on the URL (e.g. `/users/42?update`). The client primitives `useForm` and `action()` handle this automatically. The action key stays out of the page URL. When `POST /users/42?update&tab=profile` renders the page again, `useForm` and `action()` leave the address bar at `/users/42?tab=profile`, and `c.redirect('/users/42')` goes to `/users/42?tab=profile`.
Copy file name to clipboardExpand all lines: docs/reference/api/rewrites.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -10,7 +10,7 @@ URL rewrites re-dispatch a request at a different internal path without changing
10
10
11
11
Re-dispatches the current request at `destination` and returns the resulting `Response`. The browser URL does not change — this is a server-side hop. If `destination` has no query string, the original request query is preserved. If `destination` includes a query string, it replaces the original query. `destination` must start with `/`; known generated routes autocomplete via [`RewriteDestination`](#rewritedestination), while arbitrary strings remain accepted for dynamic paths.
12
12
13
-
`c.redirect()` also preserves request query params for internal destinations, but it merges rather than replaces: destination params win on collision, and request-only params are appended. Inside a [named action](../../guide/pages-routing/actions-and-forms.md#named-actions), the action key (`?delete` in `POST /users/42?delete`) is not carried over.
13
+
`c.redirect()` also preserves request query params for internal destinations, but it merges rather than replaces: destination params win on collision, and request-only params are appended. On a request to a [named action](../../guide/pages-routing/actions-and-forms.md#named-actions), the action key (`?delete` in `POST /users/42?delete`) is not carried over, whether the action or middleware redirects. A redirect after which the browser sends the same request again keeps it: `307` and `308`, and `301` or `302` after a `PUT`, `PATCH` or `DELETE`.
Copy file name to clipboardExpand all lines: packages/void/src/router/compile.ts
+26-15Lines changed: 26 additions & 15 deletions
Original file line number
Diff line number
Diff line change
@@ -33,6 +33,8 @@ export type AuthCompileOptions = {
33
33
exporttypeCompileOptions={
34
34
/** Absolute path to the hono module entry */
35
35
honoPath: string;
36
+
/** Absolute path to `hono/route` from the same Hono copy as `honoPath` */
37
+
honoRoutePath: string;
36
38
/** Absolute path to the void/_env module */
37
39
runtimeEnvPath: string;
38
40
/** Absolute path to the project's `email/` directory; enables `__emailHandler`. */
@@ -154,7 +156,7 @@ export function compileRouteTable(
154
156
lines.push(`import { AsyncLocalStorage } from "node:async_hooks";`);
155
157
lines.push(`import { Hono } from ${JSON.stringify(options.honoPath)};`);
156
158
lines.push(
157
-
`import { createRoutingRuntime as __createRoutingRuntime, createHttpRouteHandler as __createHttpRouteHandler, createErrorRenderer as __createErrorRenderer, createPageGetHandler as __createPageGetHandler, createPageMutationHandler as __createPageMutationHandler, createPagesFallback as __createPagesFallback, createModuleLoader as __createModuleLoader, createClientAssetTags as __createClientAssetTags, createManifestLoader as __createManifestLoader, createSsrFallback as __createSsrFallback, createAssetNotFound as __createAssetNotFound } from ${JSON.stringify(ROUTING_RUNTIME_PATH)};`,
159
+
`import { createRoutingRuntime as __createRoutingRuntime, createHttpRouteHandler as __createHttpRouteHandler, createErrorRenderer as __createErrorRenderer, createPageGetHandler as __createPageGetHandler, createPageMutationHandler as __createPageMutationHandler, createNamedActionMarker as __createNamedActionMarker, markRouteHandler as __markRouteHandler, createPagesFallback as __createPagesFallback, createModuleLoader as __createModuleLoader, createClientAssetTags as __createClientAssetTags, createManifestLoader as __createManifestLoader, createSsrFallback as __createSsrFallback, createAssetNotFound as __createAssetNotFound } from ${JSON.stringify(ROUTING_RUNTIME_PATH)};`,
0 commit comments