Skip to content

resolveEnv reads locals.runtime.env, which throws on @astrojs/cloudflare 14 — every /api/ask request 500s #3

Description

@hev

Problem

/api/ask returns a 500 on every request when the host site uses
@astrojs/cloudflare 14 with Astro 7. Not just the agentic path — keyword
search dies with it, because both go through the same endpoint. The Cloudflare
adapter is the one we point people at in the quick start, so on current Astro
the flagship path is broken end to end.

Astro.locals.runtime.env has been removed in Astro v6.
Use 'import { env } from "cloudflare:workers"' instead.
    at Object.get env [as env] (@astrojs/cloudflare/dist/utils/cf-helpers.js:31:15)
    at resolveEnv (@hevmind/ask/src/endpoint.ts:33)

Cause

endpoint.ts:33:

const fromRuntime = (locals as { runtime?: { env?: Record<string, string> } })?.runtime?.env ?? {};

The adapter replaced runtime.env with a getter that throws. Optional
chaining and ?? {} only defend against null/undefined, so neither helps —
the throw happens during property access, before any fallback is reached. The
defensive-looking code reads as safe and isn't.

Worth noting the failure is total rather than partial: because resolveEnv runs
on the way to any response, a site with no API key configured at all — which
should degrade to keyword-only per the README's "Server Requirements" — still
500s.

Reproduce

Astro 7 + Starlight, @astrojs/cloudflare 14, @hevmind/ask 0.3.4:

curl -X POST localhost:4321/api/ask -H 'content-type: application/json' -d '{"query":"anything"}'

Note you only reach this after working around #2 — the node:fs
import fails to resolve first, so the two bugs surface sequentially.

Fix

Read the runtime env behind a try/catch, or move to
import { env } from 'cloudflare:workers' as the error suggests. try/catch is
probably the better shape here since resolveEnv deliberately spans Node,
Cloudflare, and build-time sources and shouldn't hard-depend on a
cloudflare: module specifier that only resolves on workerd.

Same pattern appears at
endpoint.ts:52-53
for runtime.ctx (the waitUntil plumbing). I did not confirm whether that
getter throws too, but it should be audited in the same pass — if it does, the
telemetry path has the same total-failure mode.

Coverage gap

Nothing in CI runs the endpoint against workerd, which is why a
release-blocking break on the documented-primary adapter shipped. pnpm test +
typecheck + build all pass. A smoke test that boots the endpoint under
wrangler dev (or vitest with the workers pool) and asserts a keyword query
returns 200 would have caught both this and the node:fs issue.


Found while wiring 0.3.4 into a Starlight docs site. Present on main
(a3204b4), not just the 0.3.4 tag. I worked around it by switching that site to
@astrojs/node, which has neither problem.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions