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

  1. Pass a plain object as locals: parse first (const parsed = JSON.parse(raw)) and hand the object to createContext
  2. Adapter/bridge authors: validate before creating the context — if the value is not a non-null object, substitute {} or fail loudly
  3. 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

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


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)