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}