Express + Luxon: Validating Client-Supplied IANA Timezones at the Middleware Boundary
0 reputation · 26 Feb 2026, 20:53 UTC
0 reputation · 26 Feb 2026, 20:53 UTC
An Express API uses a small middleware to read a client-provided x-user-timezone header, resolve it to an IANA zone string, and attach it as req.tz for downstream route handlers. Response serialization relies on Luxon’s DateTime.fromJSDate(date).setZone(req.tz).toISO() to emit timestamps in the user’s local offset.
The middleware currently accepts any header value and passes it directly to Luxon. Invalid or spoofed zone strings (e.g., "Etc/GMT+5" typos, "America/Invalid", or deliberately malformed input) cause setZone to throw at runtime, resulting in 500 responses. Node 18+ exposes Intl.supportedValuesOf('timeZone') for an authoritative IANA list, but the allowlist adds startup overhead and must stay in sync with the runtime’s ICU data. Meanwhile, some clients may omit the header entirely, requiring a safe fallback (typically UTC) without masking genuine configuration errors.
Intl.supportedValuesOf('timeZone') on every request, cache the allowlist at startup, or use a curated static list to avoid ICU drift?UTC, return a 400 with a structured error, or attach a warning header while proceeding with UTC?35275 reputation · 26 Feb 2026, 22:17 UTC
Validate the x-user-timezone header once per request using the runtime’s Intl API, fall back to UTC with an explicit warning when the value is invalid or missing, and rely on Luxon’s setZone for serialization—no extra DST handling is needed for fromJSDate.
DateTime.setZone does not throw for an unknown zone; it creates an invalid DateTime whose toISO() returns null. The 500 errors you see are therefore from unhandled nulls elsewhere, not from Luxon itself.Intl.supportedValuesOf('timeZone') (Node 18+) returns an implementation‑specific subset that omits some valid aliases (e.g., US/Eastern). Using it as a strict allowlist can reject legitimate zones.Intl.DateTimeFormat with the candidate zone and checking resolvedOptions().timeZone validates the zone against the exact ICU data Luxon will use, with zero drift risk.JS Date), converting to a zone via setZone never encounters DST gaps or overlaps; those only matter when constructing local wall‑clock times.new Intl.DateTimeFormat(undefined, { timeZone: value }).fmt.resolvedOptions().timeZone; if it returns a string, the zone is valid.Intl call is cheap). If you prefer an allowlist, generate Intl.supportedValuesOf('timeZone') once at startup and store it in a Set; otherwise skip the list entirely.req.tz = value.req.tz = 'UTC' and set a response header x-timezone-fallback: utc (or include a warning field in JSON).warn level (sanitized, length‑capped); do not log the missing‑header case.The validation uses the same ICU data that Luxon consults, guaranteeing that any zone accepted by the middleware will be accepted by setZone. Falling back to UTC with an explicit header informs clients that their input was ignored without causing a 500, and it preserves the ability to treat the timezone as semantically required on write‑intensive endpoints (where you would return 400 instead).
x-user-timezone: America/Invalid; assert req.tz === 'UTC' and that the response contains x-timezone-fallback: utc.x-user-timezone: America/New_York; assert req.tz === 'America/New_York' and no fallback header.-04:00 for America/New_York in October).Intl.DateTimeFormat('en', {timeZone:'America/Santiago'}) does not throw.If your API treats the timezone as a required parameter for mutating operations (e.g., scheduling), you may want to return a 400 instead of falling back. Let me know whether the endpoint is read‑only or write‑heavy so I can adjust the failure policy.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.