Skip to content

Commit 17c6fac

Browse files
authored
refactor: enhance error handling and API response management (#483)
* refactor: enhance error handling and API response management - Refactored multiple API routes to utilize the new error handling utilities, ensuring consistent error responses and improved maintainability. - Improved error handling in the Claim component to log errors more effectively and provide clearer feedback on proof generation failures. - Updated axios fetcher to throw detailed errors based on API response codes. - Introduced a new error handling utility in the API layer to standardize error responses across various endpoints, including bad requests and internal errors. * chore: implement PR review feedback - Updated error handling in multiple API routes to throw detailed errors instead of returning bad requests, enhancing clarity in error reporting. - Enhanced the axios fetcher to provide more informative error messages based on API response codes. - Standardized error responses across various endpoints for better maintainability and consistency.
1 parent 493d21c commit 17c6fac

File tree

26 files changed

+425
-270
lines changed

26 files changed

+425
-270
lines changed

components/Claim/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,11 +225,17 @@ const Claim = () => {
225225
fees: migrationParams.fees.toString(),
226226
}),
227227
});
228+
229+
if (!res.ok) {
230+
const error = await res.json().catch(() => null);
231+
throw new Error(error?.error || "Failed to generate proof");
232+
}
233+
228234
const proof = await res.json();
229235

230236
setProof(proof);
231237
} catch (e) {
232-
console.log(e);
238+
console.error(e);
233239
throw new Error((e as Error)?.message);
234240
}
235241
}}

lib/api/errors.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { NextApiResponse } from "next";
2+
3+
import { ApiError, ErrorCode } from "./types/api-error";
4+
5+
export const apiError = (
6+
res: NextApiResponse,
7+
status: number,
8+
code: ErrorCode,
9+
error: string,
10+
details?: string
11+
) => {
12+
console.error(`[API Error] ${code}: ${error}`, details ?? "");
13+
const response = { error, code, details } as ApiError;
14+
res.status(status).json(response);
15+
return response;
16+
};
17+
18+
export const badRequest = (
19+
res: NextApiResponse,
20+
message: string,
21+
details?: string
22+
) => apiError(res, 400, "VALIDATION_ERROR", message, details);
23+
24+
export const notFound = (
25+
res: NextApiResponse,
26+
message: string,
27+
details?: string
28+
) => apiError(res, 404, "NOT_FOUND", message, details);
29+
30+
export const internalError = (res: NextApiResponse, err?: unknown) =>
31+
apiError(
32+
res,
33+
500,
34+
"INTERNAL_ERROR",
35+
"Internal server error",
36+
err instanceof Error ? err.message : undefined
37+
);
38+
39+
export const externalApiError = (
40+
res: NextApiResponse,
41+
service: string,
42+
details?: string
43+
) =>
44+
apiError(
45+
res,
46+
502,
47+
"EXTERNAL_API_ERROR",
48+
`Failed to fetch from ${service}`,
49+
details
50+
);
51+
52+
export const methodNotAllowed = (
53+
res: NextApiResponse,
54+
method: string,
55+
allowed: string[]
56+
) => {
57+
res.setHeader("Allow", allowed);
58+
return apiError(
59+
res,
60+
405,
61+
"METHOD_NOT_ALLOWED",
62+
`Method ${method} Not Allowed`
63+
);
64+
};

lib/api/types/api-error.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export interface ApiError {
2+
error: string;
3+
code: ErrorCode;
4+
details?: string;
5+
}
6+
7+
export type ErrorCode =
8+
| "INTERNAL_ERROR"
9+
| "VALIDATION_ERROR"
10+
| "NOT_FOUND"
11+
| "EXTERNAL_API_ERROR"
12+
| "METHOD_NOT_ALLOWED";

lib/axios.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,14 @@ export const axiosClient = defaultAxios.create({
66
});
77

88
export const fetcher = <T>(url: string) =>
9-
axiosClient.get<T>(url).then((res) => res.data);
9+
axiosClient
10+
.get<T>(url)
11+
.then((res) => res.data)
12+
.catch((err) => {
13+
const apiError = err.response?.data;
14+
if (apiError?.code) {
15+
const errorMessage = apiError.error ?? "An unknown error occurred";
16+
throw new Error(`${apiError.code}: ${errorMessage}`);
17+
}
18+
throw err;
19+
});

pages/api/account-balance/[address].tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
getBondingManagerAddress,
55
getLivepeerTokenAddress,
66
} from "@lib/api/contracts";
7+
import { badRequest, internalError, methodNotAllowed } from "@lib/api/errors";
78
import { AccountBalance } from "@lib/api/types/get-account-balance";
89
import { l2PublicClient } from "@lib/chains";
910
import { NextApiRequest, NextApiResponse } from "next";
@@ -47,15 +48,13 @@ const handler = async (
4748

4849
return res.status(200).json(accountBalance);
4950
} else {
50-
return res.status(500).end("Invalid ID");
51+
return badRequest(res, "Invalid address format");
5152
}
5253
}
5354

54-
res.setHeader("Allow", ["GET"]);
55-
return res.status(405).end(`Method ${method} Not Allowed`);
55+
return methodNotAllowed(res, method ?? "unknown", ["GET"]);
5656
} catch (err) {
57-
console.error(err);
58-
return res.status(500).json(null);
57+
return internalError(res, err);
5958
}
6059
};
6160

pages/api/changefeed.tsx

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
import { getCacheControlHeader } from "@lib/api";
2+
import {
3+
externalApiError,
4+
internalError,
5+
methodNotAllowed,
6+
} from "@lib/api/errors";
27
import { fetchWithRetry } from "@lib/fetchWithRetry";
38
import type { NextApiRequest, NextApiResponse } from "next";
49

@@ -25,27 +30,41 @@ const query = `
2530
`;
2631

2732
const changefeed = async (_req: NextApiRequest, res: NextApiResponse) => {
28-
const response = await fetchWithRetry(
29-
"https://changefeed.app/graphql",
30-
{
31-
method: "POST",
32-
headers: {
33-
"Content-Type": "application/json",
34-
Authorization: `Bearer ${process.env.CHANGEFEED_ACCESS_TOKEN!}`,
35-
},
36-
body: JSON.stringify({ query }),
37-
},
38-
{
39-
retryOnMethods: ["POST"],
40-
}
41-
);
33+
try {
34+
const method = _req.method;
35+
36+
if (method === "GET") {
37+
const response = await fetchWithRetry(
38+
"https://changefeed.app/graphql",
39+
{
40+
method: "POST",
41+
headers: {
42+
"Content-Type": "application/json",
43+
Authorization: `Bearer ${process.env.CHANGEFEED_ACCESS_TOKEN!}`,
44+
},
45+
body: JSON.stringify({ query }),
46+
},
47+
{
48+
retryOnMethods: ["POST"],
49+
}
50+
);
51+
52+
if (!response.ok) {
53+
return externalApiError(res, "changefeed.app");
54+
}
55+
56+
res.setHeader("Cache-Control", getCacheControlHeader("hour"));
4257

43-
res.setHeader("Cache-Control", getCacheControlHeader("hour"));
58+
const {
59+
data: { projectBySlugs },
60+
} = await response.json();
61+
return res.status(200).json(projectBySlugs);
62+
}
4463

45-
const {
46-
data: { projectBySlugs },
47-
} = await response.json();
48-
res.json(projectBySlugs);
64+
return methodNotAllowed(res, method ?? "unknown", ["GET"]);
65+
} catch (err) {
66+
return internalError(res, err);
67+
}
4968
};
5069

5170
export default changefeed;

pages/api/contracts.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
getLivepeerGovernorAddress,
66
getTreasuryAddress,
77
} from "@lib/api/contracts";
8+
import { internalError, methodNotAllowed } from "@lib/api/errors";
89
import { ContractInfo } from "@lib/api/types/get-contract-info";
910
import { CHAIN_INFO, DEFAULT_CHAIN_ID } from "@lib/chains";
1011
import { NextApiRequest, NextApiResponse } from "next";
@@ -126,11 +127,9 @@ const handler = async (
126127
return res.status(200).json(contractsInfo);
127128
}
128129

129-
res.setHeader("Allow", ["GET"]);
130-
return res.status(405).end(`Method ${method} Not Allowed`);
130+
return methodNotAllowed(res, method ?? "unknown", ["GET"]);
131131
} catch (err) {
132-
console.error(err);
133-
return res.status(500).json(null);
132+
return internalError(res, err);
134133
}
135134
};
136135

pages/api/current-round.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getCacheControlHeader, getCurrentRound } from "@lib/api";
2+
import { internalError, methodNotAllowed } from "@lib/api/errors";
23
import { CurrentRoundInfo } from "@lib/api/types/get-current-round";
34
import { l1PublicClient } from "@lib/chains";
45
import { NextApiRequest, NextApiResponse } from "next";
@@ -19,11 +20,11 @@ const handler = async (
1920
const currentRound = protocol?.currentRound;
2021

2122
if (!currentRound) {
22-
return res.status(500).end("No current round found");
23+
throw new Error("No current round found");
2324
}
2425

2526
if (!_meta?.block) {
26-
return res.status(500).end("No block number found");
27+
throw new Error("No block number found");
2728
}
2829

2930
const { id, startBlock, initialized } = currentRound;
@@ -42,11 +43,9 @@ const handler = async (
4243
return res.status(200).json(roundInfo);
4344
}
4445

45-
res.setHeader("Allow", ["GET"]);
46-
return res.status(405).end(`Method ${method} Not Allowed`);
46+
return methodNotAllowed(res, method ?? "unknown", ["GET"]);
4747
} catch (err) {
48-
console.error(err);
49-
return res.status(500).json(null);
48+
return internalError(res, err);
5049
}
5150
};
5251

pages/api/ens-data/[address].tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getCacheControlHeader } from "@lib/api";
22
import { getEnsForAddress } from "@lib/api/ens";
3+
import { badRequest, internalError, methodNotAllowed } from "@lib/api/errors";
34
import { EnsIdentity } from "@lib/api/types/get-ens";
45
import { NextApiRequest, NextApiResponse } from "next";
56
import { Address, isAddress } from "viem";
@@ -30,15 +31,13 @@ const handler = async (
3031

3132
return res.status(200).json(ens);
3233
} else {
33-
return res.status(500).end("Invalid ID");
34+
return badRequest(res, "Invalid address format");
3435
}
3536
}
3637

37-
res.setHeader("Allow", ["GET"]);
38-
return res.status(405).end(`Method ${method} Not Allowed`);
38+
return methodNotAllowed(res, method ?? "unknown", ["GET"]);
3939
} catch (err) {
40-
console.error(err);
41-
return res.status(500).json(null);
40+
return internalError(res, err);
4241
}
4342
};
4443

pages/api/ens-data/image/[name].tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
import { getCacheControlHeader } from "@lib/api";
2+
import {
3+
badRequest,
4+
internalError,
5+
methodNotAllowed,
6+
notFound,
7+
} from "@lib/api/errors";
28
import { l1PublicClient } from "@lib/chains";
39
import { parseArweaveTxId, parseCid } from "livepeer/utils";
410
import { NextApiRequest, NextApiResponse } from "next";
@@ -47,18 +53,16 @@ const handler = async (
4753
return res.end(Buffer.from(arrayBuffer));
4854
} catch (e) {
4955
console.error(e);
50-
return res.status(404).end("Invalid name");
56+
return notFound(res, "ENS avatar not found");
5157
}
5258
} else {
53-
return res.status(500).end("Invalid name");
59+
return badRequest(res, "Invalid ENS name");
5460
}
5561
}
5662

57-
res.setHeader("Allow", ["GET"]);
58-
return res.status(405).end(`Method ${method} Not Allowed`);
63+
return methodNotAllowed(res, method ?? "unknown", ["GET"]);
5964
} catch (err) {
60-
console.error(err);
61-
return res.status(500).json(null);
65+
return internalError(res, err);
6266
}
6367
};
6468

0 commit comments

Comments
 (0)