Files
webmail/lib/admin/config-read-only-response.ts
T
Linus Rath 740a892590 fix: answer 503 for admin and setup saves while the shared store is down #1066
Config, policy, branding, OAuth setup, password and setup wizard writes
used to answer 500 when the shared store couldn't be reached, and the
wizard's token check failed outside any handler. They answer 503 with
Retry-After now, through the helper that already turned a read-only
refusal into 409. The step-session-secret test's wizard mock gains
wizardSessionRefusal, which the setup routes now call.
2026-11-01 00:02:57 +01:00

35 lines
1.5 KiB
TypeScript

import { NextResponse } from 'next/server';
import { isStoreUnavailableError } from '@/lib/shared-store';
/**
* The answer an admin or setup route gives when a config write failed for
* a reason the admin should see, or null for any other error, which the
* route keeps reporting as a 500:
*
* - 409 when the write hit ADMIN_CONFIG_READONLY, carrying the
* ConfigReadOnlyError message so the panel can show why nothing was
* saved (#1249).
* - 503 with Retry-After when the shared store can't be reached (#1066):
* nothing was saved, and trying again in a moment may work.
*
* By the error's name, not instanceof: the config manager on globalThis
* comes from another bundle's copy of lib/admin/paths.ts (the
* instrumentation creates it), whose ConfigReadOnlyError is a different
* class, and a read-only refusal then answered 500.
*/
export function configWriteErrorResponse(error: unknown): NextResponse | null {
if (error instanceof Error && error.name === 'ConfigReadOnlyError') {
return NextResponse.json({ error: error.message }, { status: 409 });
}
if (isStoreUnavailableError(error)) {
return NextResponse.json(
{ error: 'Nothing was saved: the shared store can\'t be reached right now. Try again in a moment.' },
{ status: 503, headers: { 'Retry-After': '10' } },
);
}
return null;
}
/** The same as configWriteErrorResponse; the name the admin routes use. */
export const configReadOnlyResponse = configWriteErrorResponse;