xtap-pool / space /src /consumer-errors.ts
osolmaz's picture
fix(consumer): prepare source bases before reads
1b4196c verified
Raw History Blame Contribute Delete
2.82 kB
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;
}