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;
}