File size: 2,822 Bytes
ce0aad2 1b4196c ce0aad2 1b4196c ce0aad2 1b4196c ce0aad2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | import { ZodError } from "zod";
import { ConsumerSourceNotReady } from "./consumer-source.js";
import {
ConsumerContractChanged,
ExpiredConsumerCursor,
InvalidConsumerCursor,
} from "./consumer-cursor.js";
import { ConsumerBootstrapRequired } from "./consumer-index-state.js";
import { OversizedConsumerSource } from "./consumer-page.js";
import { HistoricalReadLimitError } from "./historical-unit-reader.js";
export class ConsumerHttpError extends Error {
constructor(
readonly status: 400 | 401 | 403 | 409 | 410 | 413 | 429 | 502 | 503,
readonly code: string,
message: string,
readonly recovery?: unknown,
) {
super(message);
}
}
export function consumerError(error: unknown): ConsumerHttpError {
if (error instanceof ConsumerHttpError) return error;
if (error instanceof InvalidConsumerCursor || error instanceof ZodError)
return new ConsumerHttpError(400, "invalid_request", "Invalid consumer query or cursor.");
if (error instanceof ConsumerContractChanged)
return new ConsumerHttpError(409, "contract_changed", error.message, {
action: "explicit_bootstrap",
});
if (error instanceof ExpiredConsumerCursor)
return new ConsumerHttpError(410, "cursor_expired", error.message, {
action: "explicit_bootstrap",
});
if (isSourceStateError(error)) return sourceStateError(error);
if (isSourceLimit(error))
return new ConsumerHttpError(
413,
"source_item_too_large",
"A complete source item exceeds the read bound.",
);
return new ConsumerHttpError(
502,
"source_unavailable",
"The verified source could not be read. Retry the same cursor.",
);
}
export function consumerErrorResponse(error: unknown): Response {
const failure = consumerError(error);
return Response.json(
{
error: {
code: failure.code,
message: failure.message,
...(failure.recovery === undefined ? {} : { recovery: failure.recovery }),
},
},
{
status: failure.status,
headers: {
"Cache-Control": "no-store",
...(failure.status === 429 || failure.status === 503 ? { "Retry-After": "5" } : {}),
},
},
);
}
function isSourceStateError(
error: unknown,
): error is ConsumerSourceNotReady | ConsumerBootstrapRequired {
return error instanceof ConsumerSourceNotReady || error instanceof ConsumerBootstrapRequired;
}
function sourceStateError(
error: ConsumerSourceNotReady | ConsumerBootstrapRequired,
): ConsumerHttpError {
const code =
error instanceof ConsumerSourceNotReady ? "source_not_ready" : "projection_unavailable";
return new ConsumerHttpError(503, code, error.message);
}
function isSourceLimit(error: unknown): boolean {
return error instanceof OversizedConsumerSource || error instanceof HistoricalReadLimitError;
}
|