| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { isBillingVerificationCode } from './_shared/entitlement-check'; |
|
|
| |
| |
| |
| |
| |
| function isNetworkError(error: unknown): boolean { |
| if (!(error instanceof TypeError)) return false; |
| const msg = error.message.toLowerCase(); |
| return msg.includes('fetch') || msg.includes('network') || msg.includes('connect') || msg.includes('econnrefused') || msg.includes('enotfound') || msg.includes('socket'); |
| } |
|
|
| |
| |
| |
| |
| |
| function jsonMessageResponse(message: string, status: number, extras?: Record<string, unknown>, headers?: Record<string, string>): Response { |
| return new Response(JSON.stringify({ message, ...(extras ?? {}) }), { |
| status, |
| headers: { 'Content-Type': 'application/json', ...(headers ?? {}) }, |
| }); |
| } |
|
|
| export function mapErrorToResponse(error: unknown, _req: Request): Response { |
| |
| if (error instanceof Error && 'statusCode' in error) { |
| const statusCode = (error as Error & { statusCode: number }).statusCode; |
| |
| |
| const retryAfter = (statusCode === 429 || statusCode === 503) && 'retryAfter' in error ? Number((error as Error & { retryAfter: number }).retryAfter) : null; |
| const billingCodeCandidate = 'billingVerificationCode' in error |
| ? (error as Error & { billingVerificationCode: unknown }).billingVerificationCode |
| : null; |
| const billingVerificationCode = isBillingVerificationCode(billingCodeCandidate) |
| ? billingCodeCandidate |
| : null; |
| const exposesRetryableUnavailable = statusCode === 503 |
| && retryAfter != null |
| && Number.isFinite(retryAfter) |
| && (error as Error & { exposeMessage?: boolean }).exposeMessage === true; |
| const message = (statusCode >= 400 && statusCode < 500) || exposesRetryableUnavailable ? error.message : 'Internal server error'; |
| const extras: Record<string, unknown> = {}; |
| const headers: Record<string, string> = {}; |
|
|
| |
| if (retryAfter != null && Number.isFinite(retryAfter)) { |
| extras.retryAfter = retryAfter; |
| headers['Retry-After'] = String(retryAfter); |
| } |
| if (billingVerificationCode) { |
| extras.code = billingVerificationCode; |
| headers['X-Billing-Verification'] = billingVerificationCode; |
| } |
|
|
| if (statusCode >= 500) { |
| |
| const apiBody = 'body' in error ? String((error as any).body).slice(0, 500) : ''; |
| console.error(`[error-mapper] ${statusCode}:`, error.message, apiBody ? `| body: ${apiBody}` : ''); |
| } |
|
|
| return jsonMessageResponse( |
| message, |
| statusCode, |
| Object.keys(extras).length > 0 ? extras : undefined, |
| Object.keys(headers).length > 0 ? headers : undefined, |
| ); |
| } |
|
|
| |
| if (error instanceof SyntaxError) { |
| return jsonMessageResponse('Invalid request body', 400); |
| } |
|
|
| |
| if (isNetworkError(error)) { |
| console.error('[error-mapper] Network error (502):', (error as Error).message); |
| return jsonMessageResponse('Upstream unavailable', 502); |
| } |
|
|
| |
| console.error('[error-mapper] Unhandled error:', error instanceof Error ? error.message : error); |
| return jsonMessageResponse('Internal server error', 500); |
| } |
|
|