Summary
When the EmDash runtime fails to initialize (broken DB binding, downstream failure), the content API responds 401 UNAUTHORIZED / "Authentication required" to requests carrying a perfectly valid token. The NOT_CONFIGURED 500 that exists for exactly this case is unreachable, because the permission check runs before it.
This misattributes a server fault to the caller's credentials. In our case it cost about half a day of debugging a token that was never broken, and it breaks retry logic: clients correctly treat 4xx as non-retryable.
Affected: verified in 0.1.0 and 0.29.0 (latest). Present in both; line numbers below for each.
Reproduction
- Deploy an EmDash site where runtime init fails (e.g. break the D1 binding).
GET /_emdash/api/content/posts?status=published&limit=75 with a valid Authorization: Bearer <token>.
- Observe:
401 {"error":{"code":"UNAUTHORIZED","message":"Authentication required"}}, returned in ~0.15s.
GET / still returns 200 — the Worker is alive.
GET /_emdash/api/schema under the same fault correctly returns 5xx.
Step 5 is the tell: the same codebase handles this correctly in one place and incorrectly in another.
Root cause
Three steps:
1. The middleware swallows the init error, so locals.emdash is never set:
// 0.29.0 src/astro/middleware.ts:796 (0.1.0: :323)
} catch (error) {
console.error("EmDash middleware error:", error);
2. Auth middleware bails out before it ever looks at the token, leaving locals.user undefined:
// 0.29.0 src/astro/middleware/auth.ts:350 (0.1.0: :273)
if (!emdash?.db) {
return next();
}
3. The content route checks permission before checking initialization:
// 0.29.0 src/astro/routes/api/content/[collection]/index.ts
const denied = requirePerm(user, "content:read"); // line 20
if (denied) return denied; // -> 401, user is undefined
// ...
if (!emdash?.handleContentList) {
return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500); // line 27 — unreachable
}
requirePerm (src/api/authorize.ts) returns apiError("UNAUTHORIZED", "Authentication required", 401) when !user.
The 500 sits seven lines too late and never fires in the failure it was written for.
Suggested fix
The schema routes already do it right — mirror them:
// src/astro/routes/api/schema/index.ts:25-28 (0.29.0)
const dbErr = requireDb(emdash?.db);
if (dbErr) return dbErr;
const denied = requirePerm(user, "schema:read");
requireDb is already exported from #api/error.js. Applying the same order to the content routes (9 of 10 have the inverted order) would fix this without new concepts.
Optionally: the swallowed error in middleware.ts is the only trace of the real fault. Surfacing it in the 5xx body — or at least behind a debug flag — would make this diagnosable from the outside.
Why it matters
A 401 tells the caller "your credentials are wrong". Clients act on that: ours excludes 4xx from retries by design, so a transient server fault aborts the build immediately and points the on-call person at the wrong thing.
Summary
When the EmDash runtime fails to initialize (broken DB binding, downstream failure), the content API responds
401 UNAUTHORIZED/ "Authentication required" to requests carrying a perfectly valid token. TheNOT_CONFIGURED500 that exists for exactly this case is unreachable, because the permission check runs before it.This misattributes a server fault to the caller's credentials. In our case it cost about half a day of debugging a token that was never broken, and it breaks retry logic: clients correctly treat 4xx as non-retryable.
Affected: verified in 0.1.0 and 0.29.0 (latest). Present in both; line numbers below for each.
Reproduction
GET /_emdash/api/content/posts?status=published&limit=75with a validAuthorization: Bearer <token>.401 {"error":{"code":"UNAUTHORIZED","message":"Authentication required"}}, returned in ~0.15s.GET /still returns 200 — the Worker is alive.GET /_emdash/api/schemaunder the same fault correctly returns 5xx.Step 5 is the tell: the same codebase handles this correctly in one place and incorrectly in another.
Root cause
Three steps:
1. The middleware swallows the init error, so
locals.emdashis never set:2. Auth middleware bails out before it ever looks at the token, leaving
locals.userundefined:3. The content route checks permission before checking initialization:
requirePerm(src/api/authorize.ts) returnsapiError("UNAUTHORIZED", "Authentication required", 401)when!user.The 500 sits seven lines too late and never fires in the failure it was written for.
Suggested fix
The schema routes already do it right — mirror them:
requireDbis already exported from#api/error.js. Applying the same order to the content routes (9 of 10 have the inverted order) would fix this without new concepts.Optionally: the swallowed error in
middleware.tsis the only trace of the real fault. Surfacing it in the 5xx body — or at least behind a debug flag — would make this diagnosable from the outside.Why it matters
A 401 tells the caller "your credentials are wrong". Clients act on that: ours excludes 4xx from retries by design, so a transient server fault aborts the build immediately and points the on-call person at the wrong thing.