fix(intake-public): read request headers via next/headers, not param
Phase 8a Task 7 shipped the headers argument as part of the Server Action signature. Server Actions serialise their arguments through Next's RSC protocol, and a Headers instance does not survive that — it ends up as something with no .get() and the action explodes with 'headers.get is not a function' the first time the wizard runs. Fix: drop the headers parameter from the action and read the inbound request's headers via next/headers inside the action. The client component no longer needs to construct a Headers instance either. This was caught the first time a real browser hit the wizard — the integration tests in Task 17 mock the request and never serialised a Headers through RSC.
This commit is contained in:
parent
a4410d3c56
commit
b02ebb7cf6
2 changed files with 10 additions and 8 deletions
|
|
@ -1,5 +1,7 @@
|
|||
"use server";
|
||||
|
||||
import { headers } from "next/headers";
|
||||
|
||||
import { clientIpFromHeaders } from "@/lib/http/client-ip";
|
||||
import { allow } from "@/lib/intake-links/rate-limit";
|
||||
import { validateIntakeLinkForStep } from "@/lib/intake-links";
|
||||
|
|
@ -14,6 +16,12 @@ import type { Turn } from "@/lib/intake/turn";
|
|||
* shape check happen here, in that order, before any token lookup touches
|
||||
* the database. A bad-shape request never charges a rate-limit bucket.
|
||||
*
|
||||
* The client IP is read via `next/headers` rather than passed in. Server
|
||||
* Actions serialise their arguments through Next's RSC protocol, and
|
||||
* `Headers` instances do not survive that — passing one as a parameter
|
||||
* ended up as something with no `.get()`. `headers()` is the supported way
|
||||
* for a Server Action to read the inbound request's headers.
|
||||
*
|
||||
* Returns the same shape as `intakeStepAction` so the wizard code does not
|
||||
* branch on which side it is on. The "no longer active" wording is the same
|
||||
* as "daily cap reached" intentionally — the spec's §3 "doesn't shout"
|
||||
|
|
@ -24,14 +32,13 @@ export async function intakeStepPublicAction(input: {
|
|||
token: string;
|
||||
turns: Turn[];
|
||||
finishNow: boolean;
|
||||
/** Caller passes its own request headers so the limiter can key on IP. */
|
||||
headers: Headers;
|
||||
}): Promise<InterviewStep> {
|
||||
if (!isIntakeToken(input.token)) {
|
||||
return { kind: "unavailable", message: "This link is no longer active." };
|
||||
}
|
||||
|
||||
const ip = clientIpFromHeaders(input.headers);
|
||||
const requestHeaders = await headers();
|
||||
const ip = clientIpFromHeaders(requestHeaders);
|
||||
if (!allow(`${input.token}:${ip}`)) {
|
||||
return { kind: "unavailable", message: "Too many requests right now." };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,11 +37,6 @@ export function IntakePublic({
|
|||
token,
|
||||
turns: nextTurns,
|
||||
finishNow,
|
||||
// The Server Action reads Headers() inside Next; we have no Headers
|
||||
// here, but the limiter uses an opaque key and the missing client IP
|
||||
// collapses all callers from this browser into one bucket. That's
|
||||
// fine — it is the worst case, not the typical one.
|
||||
headers: new Headers(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue