withastro/astro · error · AstroError
LocalsNotAnObject
LocalsNotAnObject
Error message
`locals` can only be assigned to an object. Other values like numbers, strings, etc. are not accepted.
What it means
On contexts built by createContext(), the locals getter validates that the underlying locals value is an object and throws AstroErrorData.LocalsNotAnObject otherwise. The locals value comes from whoever created the context — on Vercel/Netlify it is deserialized from the locals forwarding header (x-astro-locals) — so a scalar (string, number, boolean, null) in that channel makes the very first access to context.locals throw.
Solutions
- Pass a plain object as locals: parse first (const parsed = JSON.parse(raw)) and hand the object to createContext
- Adapter/bridge authors: validate before creating the context — if the value is not a non-null object, substitute {} or fail loudly
- Never overwrite the platform's locals forwarding header with non-object JSON
Example fix
// before (adapter bridge)
const ctx = createContext({ request, locals: request.headers.get('x-astro-locals') });
// after
const raw = request.headers.get('x-astro-locals');
const parsed = raw ? JSON.parse(raw) : {};
const ctx = createContext({ request, locals: parsed && typeof parsed === 'object' ? parsed : {} }); Defensive patterns
Strategy: validation
Validate before calling
const raw = request.headers.get('x-astro-locals');
let locals: App.Locals = {};
if (raw) {
const parsed: unknown = JSON.parse(raw);
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
locals = parsed as App.Locals;
}
}
const ctx = createContext({ request, locals }); Type guard
const isLocalsObject = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value);
Prevention
- Bridge authors: validate the parsed locals shape before creating a context
- Keep everything stored in locals as object properties; never let a scalar ride the forwarding header
- Do not overwrite or re-encode the platform's locals header in proxies or tests
When it happens
Trigger: An adapter or hand-written bridge passes a non-object into createContext({ locals }): a JSON string that was not parsed, a scalar value from the x-astro-locals header (e.g. overwritten by a proxy or test fixture to '42' or 'abc'), or locals arriving as an array.
Common situations: Edge middleware to function handoff where the locals header gets overwritten or double-encoded; custom integrations/tests calling createContext with a serialized locals string; middleware that stores a scalar into a header-based channel that later feeds locals.
Related errors
- The passed value can't be serialized.
- StaticClientAddressNotAvailable
- AdapterSupportOutputMismatch
- `Astro.session` was accessed but no session storage is…
- ClientAddressNotAvailable
AI-assisted analysis of withastro/astro@e294953aa8 (2026-08-18).
Data as JSON: /api/errors/9891a41dde6ab0b1.
Report an issue: GitHub.
Appendix: source
Thrown at packages/astro/src/core/middleware/index.ts:135
get preferredLocaleList(): string[] | undefined {
return (preferredLocaleList ??= computePreferredLocaleList(request, userDefinedLocales));
},
get currentLocale(): string | undefined {
return (currentLocale ??= computeCurrentLocale(route, userDefinedLocales, defaultLocale));
},
url,
get originPathname() {
return getOriginPathname(request);
},
get clientAddress() {
if (clientAddress) {
return clientAddress;
}
throw new AstroError(AstroErrorData.StaticClientAddressNotAvailable);
},
get locals() {
if (typeof locals !== 'object') {
throw new AstroError(AstroErrorData.LocalsNotAnObject);
}
return locals;
},
set locals(_) {
throw new AstroError(AstroErrorData.LocalsReassigned);
},
session: undefined,
cache: new DisabledAstroCache(),
csp: undefined,
logger: {
info() {},
warn() {},
error() {},
},
};
return Object.assign(context, {
getActionResult: createGetActionResult(context.locals),
callAction: createCallAction(context),View on GitHub (pinned to e294953aa8)