Page actions (#1847) landed and the shape is right — a page declares action, gets its own non-GET request, and returns either a redirect or a context merge that repopulates the form it drew. I went to adopt it across five forms in a real app and got stopped on the first one: an action cannot set a cookie, so it cannot sign anyone in.
The contract, from page-action.js
const result = await context.action({ request, form, params, query, cookies })
if (result && typeof result === 'object') {
const to = result.redirect
if (typeof to === 'string' && to) return { redirect: to, ran: true }
Object.assign(context, result)
}
return { ran: true }and the redirect it produces:
export function actionRedirectResponse(to) {
return new Response(null, {
status: 303,
headers: { Location: to, 'Cache-Control': 'no-store' },
})
}PageActionResult is { redirect?: string, ran: boolean }. An action reads cookies on the way in and has no way to write one on the way out — no header hook, no Set-Cookie, and actionRedirectResponse builds its headers from a literal.
Why that blocks the canonical case
Sign-in is the form everyone reaches for first, and it is defined by establishing a session. The no-JS flow is: POST credentials → verify → set the session cookie → 303 to the destination. Steps 1, 2 and 4 are expressible; step 3 is not.
The same applies to sign-up, "remember me", accepting an invite that pins a tenant, and switching a locale or theme that must survive the redirect. Everything that is not a session cookie has a workaround; a session cookie does not, because the browser must receive it on the response that redirects.
In my app this is the difference between four of five forms adopting page actions and all five. Sign-in and sign-up have to keep posting to a separate JSON endpoint that can set the cookie, which leaves exactly the split #1847 set out to remove: one handler for the no-JS path, another for the enhanced one.
Suggested shape
Anything that lets an action contribute response headers would do. In rough order of how little it changes:
- A
cookiesfield on the result, mirroring the read side:
Symmetric with what the action already receives, and needs noreturn { redirect: '/dashboard', cookies: { loghq_token: { value: token, maxAge: 2592000, sameSite: 'Lax', httpOnly: true, path: '/' } } }Responsein the action. headerson the result, merged into whatever response is produced — more general, less guided.- Let an action return a
Responseand use it verbatim. Most flexible, but it moves redirect-status and cache-header decisions back to every call site, whichactionRedirectResponsewas written to centralise.
(1) fits the existing design best: PageActionContext already hands cookies in, so handing them back is the obvious other half.
Environment
- stx 0.2.176 built from source, bun 1.3.14, macOS 15
- 38-view Stacks app used as the e2e surface for stx; five forms, all currently
preventDefault+fetch, which is what #1847 measured