grain.social is a photo sharing platform built on atproto.
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 11export function onError(err: unknown): Response { 12 if (err instanceof BadRequestError) { 13 return errorResponse(err.message, 400); 14 } 15 if (err instanceof ServerError) { 16 return errorResponse(err.message, 500); 17 } 18 if (err instanceof NotFoundError) { 19 return errorResponse(err.message, 404); 20 } 21 if (err instanceof UnauthorizedError) { 22 const ctx = err.ctx; 23 return ctx.redirect(OAUTH_ROUTES.loginPage); 24 } 25 if (err instanceof RateLimitError) { 26 const now = new Date(); 27 const future = new Date(now.getTime() + (err.retryAfter ?? 0) * 1000); 28 const duration = intervalToDuration({ start: now, end: future }); 29 const formatted = formatDuration(duration, { 30 format: ["minutes", "seconds"], 31 }); 32 return new Response( 33 `Too many requests. Retry in ${formatted}.`, 34 { 35 status: 429, 36 headers: { 37 ...(err.retryAfter && { "Retry-After": err.retryAfter.toString() }), 38 "Content-Type": "text/plain; charset=utf-8", 39 }, 40 }, 41 ); 42 } 43 console.error("Unhandled error:", err); 44 return errorResponse("Internal Server Error", 500); 45} 46 47export class NotFoundError extends Error { 48 constructor(message = "Not Found") { 49 super(message); 50 this.name = "NotFoundError"; 51 } 52} 53 54export const ServerError = class extends Error { 55 constructor(message = "Internal Server Error") { 56 super(message); 57 this.name = "ServerError"; 58 } 59}; 60 61export class BadRequestError extends Error { 62 constructor(message: string = "Bad Request") { 63 super(message); 64 this.name = "BadRequestError"; 65 } 66}