grain.social is a photo sharing platform built on atproto.
at main 114 lines 2.9 kB view raw
1import { OAUTH_ROUTES, RateLimitError, UnauthorizedError } from "@bigmoves/bff"; 2import { formatDuration, intervalToDuration } from "date-fns"; 3 4function errorResponse(message: string, status: number): Response { 5 return new Response(message, { 6 status, 7 headers: { "Content-Type": "text/plain; charset=utf-8" }, 8 }); 9} 10 11function jsonErrorResponse(err: XRPCError): Response { 12 return new Response(JSON.stringify(err.toJSON()), { 13 status: err.status, 14 headers: { 15 "Content-Type": "application/json", 16 }, 17 }); 18} 19 20export function onError(err: unknown): Response { 21 if (err instanceof XRPCError) { 22 return jsonErrorResponse(err); 23 } 24 if (err instanceof BadRequestError) { 25 return errorResponse(err.message, 400); 26 } 27 if (err instanceof ServerError) { 28 return errorResponse(err.message, 500); 29 } 30 if (err instanceof NotFoundError) { 31 return errorResponse(err.message, 404); 32 } 33 if (err instanceof UnauthorizedError) { 34 const ctx = err.ctx; 35 return ctx.redirect(OAUTH_ROUTES.loginPage); 36 } 37 if (err instanceof RateLimitError) { 38 const now = new Date(); 39 const future = new Date(now.getTime() + (err.retryAfter ?? 0) * 1000); 40 const duration = intervalToDuration({ start: now, end: future }); 41 const formatted = formatDuration(duration, { 42 format: ["minutes", "seconds"], 43 }); 44 return new Response( 45 `Too many requests. Retry in ${formatted}.`, 46 { 47 status: 429, 48 headers: { 49 ...(err.retryAfter && { "Retry-After": err.retryAfter.toString() }), 50 "Content-Type": "text/plain; charset=utf-8", 51 }, 52 }, 53 ); 54 } 55 console.error("Unhandled error:", err); 56 return errorResponse("Internal Server Error", 500); 57} 58 59export class NotFoundError extends Error { 60 constructor(message = "Not Found") { 61 super(message); 62 this.name = "NotFoundError"; 63 } 64} 65 66export const ServerError = class extends Error { 67 constructor(message = "Internal Server Error") { 68 super(message); 69 this.name = "ServerError"; 70 } 71}; 72 73export class BadRequestError extends Error { 74 constructor(message: string = "Bad Request") { 75 super(message); 76 this.name = "BadRequestError"; 77 } 78} 79 80const XRPCErrorCodes = { 81 InvalidRequest: 400, 82 NotFound: 404, 83 InternalServerError: 500, 84 AuthenticationRequired: 401, 85 PayloadTooLarge: 413, 86} as const; 87 88type XRPCErrorCode = keyof typeof XRPCErrorCodes; 89 90export class XRPCError extends Error { 91 code: XRPCErrorCode; 92 error?: unknown; 93 94 constructor(code: XRPCErrorCode, error?: unknown) { 95 super(typeof error === "string" ? error : code); 96 this.name = "XRPCError"; 97 this.code = code; 98 this.error = error; 99 if (Error.captureStackTrace) { 100 Error.captureStackTrace(this, XRPCError); 101 } 102 } 103 104 get status(): number { 105 return XRPCErrorCodes[this.code]; 106 } 107 108 toJSON() { 109 return { 110 error: this.code, 111 message: this.message, 112 }; 113 } 114}