Skip to content

Commit df6c94d

Browse files
feat: create uniform error structure (#115)
Signed-off-by: james-a-morris <[email protected]>
1 parent f2b8e95 commit df6c94d

File tree

19 files changed

+118
-62
lines changed

19 files changed

+118
-62
lines changed

apps/node/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"@repo/indexer-api": "workspace:*",
3434
"@repo/persistence-example": "workspace:*",
3535
"@repo/template": "workspace:*",
36+
"@repo/error-handling": "workspace:*",
3637
"@uma/logger": "^1.3.0",
3738
"dotenv": "^16.4.5",
3839
"source-map-support": "^0.5.21",

apps/node/src/app.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import "source-map-support/register";
22

33
import dotenv from "dotenv";
4-
import assert from "assert";
54

65
import * as Template from "@repo/template";
76
import * as Indexer from "@repo/indexer";
87
import * as PersistenceExample from "@repo/persistence-example";
98
import * as IndexerApi from "@repo/indexer-api";
9+
import { assert } from "@repo/error-handling";
1010
import { Logger } from "@uma/logger";
1111

1212
dotenv.config();

packages/error-handling/src/errors/IndexerHTTPError.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,11 @@ export { StatusCodes };
1010
*/
1111
export abstract class IndexerHTTPError extends IndexerError {
1212
constructor(
13-
private readonly httpStatusCode: StatusCodes,
13+
public readonly httpStatusCode: StatusCodes,
1414
errorName: string,
1515
errorMessage: string,
1616
errorData?: Record<string, string>,
1717
) {
1818
super(errorName, errorMessage, errorData);
1919
}
20-
21-
/**
22-
* A function used by `JSON.stringify` to specify which data will be serialized
23-
* @returns A formatted JSON
24-
*/
25-
public toJSON(): Record<string, unknown> {
26-
return {
27-
statusCode: this.httpStatusCode,
28-
...super.toJSON(),
29-
};
30-
}
3120
}

packages/indexer-api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"author": "",
2323
"license": "ISC",
2424
"dependencies": {
25+
"@repo/error-handling": "workspace:*",
2526
"@repo/indexer": "workspace:*",
2627
"@repo/webhooks": "workspace:*",
2728
"@repo/indexer-database": "workspace:*",

packages/indexer-api/src/dtos/deposits.dto.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import * as s from "superstruct";
22
import { entities } from "@repo/indexer-database";
33

4-
const stringToInt = s.coerce(s.number(), s.string(), (value) =>
5-
parseInt(value),
6-
);
4+
const stringToInt = s.coerce(s.number(), s.string(), (value) => {
5+
// Ensure the value is a valid integer string
6+
if (!/^-?\d+$/.test(value)) {
7+
return value;
8+
}
9+
return parseInt(value, 10);
10+
});
711

812
export const DepositsParams = s.object({
913
depositor: s.optional(s.string()),
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {
2+
isIndexerError,
3+
isIndexerHTTPError,
4+
StatusCodes,
5+
} from "@repo/error-handling";
6+
import { Request, Response, NextFunction } from "express";
7+
import { isHttpError } from "./express-app";
8+
import { StructError } from "superstruct";
9+
10+
const DEFAULT_STATUS = StatusCodes.BAD_REQUEST;
11+
12+
const errorHandler = (
13+
err: unknown,
14+
req: Request,
15+
res: Response,
16+
_: NextFunction,
17+
): void => {
18+
// At a base level we need to confirm that this isn't a valid
19+
// passthrough - if so ignore
20+
if (isIndexerError(err)) {
21+
// If we have a custom sub-type to specify the error code, use it
22+
// otherwise default to a status 400
23+
const httpStatus = isIndexerHTTPError(err)
24+
? err.httpStatusCode
25+
: DEFAULT_STATUS;
26+
res.status(httpStatus).json(err.toJSON());
27+
} else if (isHttpError(err)) {
28+
res.status(err.status ?? DEFAULT_STATUS).json({
29+
message: err.message,
30+
error: "NavigationError",
31+
});
32+
} else if (err instanceof StructError) {
33+
res.status(StatusCodes.BAD_REQUEST).json({
34+
error: "ValidationError",
35+
message: err.message,
36+
});
37+
} else if (err instanceof Error) {
38+
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
39+
error: "UnknownError",
40+
message: err.message,
41+
});
42+
}
43+
};
44+
45+
export default errorHandler;

packages/indexer-api/src/express-app.ts

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import cors from "cors";
22
import bodyParser from "body-parser";
33
import type { Request, Response, NextFunction, Express, Router } from "express";
44
import express from "express";
5+
import errorHandler from "./error-handler";
56

67
export class HttpError extends Error {
78
status?: number;
@@ -31,33 +32,14 @@ export function ExpressApp(routers: RouterConfigs): Express {
3132
});
3233

3334
app.use(function (_: Request, __: Response, next: NextFunction) {
34-
const error = new HttpError("Not Found");
35+
const error = new HttpError("Route does not exist.");
3536
error["status"] = 404;
3637
next(error);
3738
});
3839

39-
app.use(function (
40-
err: HttpError | Error,
41-
req: Request,
42-
res: Response,
43-
// this needs to be included even if unused, since 4 param call triggers error handler
44-
_: NextFunction,
45-
) {
46-
const request = {
47-
method: req.method,
48-
path: req.path,
49-
body: req.body,
50-
};
51-
let status = 500;
52-
if (isHttpError(err)) {
53-
status = err.status ?? status;
54-
}
55-
res.status(status).json({
56-
message: err.message,
57-
request,
58-
stack: err.stack,
59-
});
60-
});
40+
// Register an error handler as the last part of the
41+
// express pipeline
42+
app.use(errorHandler);
6143

6244
return app;
6345
}

packages/indexer-api/src/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import assert from "assert";
1+
import { assert } from "@repo/error-handling";
22
import { ExpressApp } from "./express-app";
33
import { createDataSource, DatabaseConfig } from "@repo/indexer-database";
44
import * as routers from "./routers";

packages/indexer-api/src/services/balances.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import assert from "assert";
1+
import { assert } from "@repo/error-handling";
22
import Redis from "ioredis";
33
import * as Indexer from "@repo/indexer";
44
import {
Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1-
import { HttpError } from "../express-app";
2-
import { HttpStatus } from "../model/httpStatus";
1+
import { IndexerHTTPError, StatusCodes } from "@repo/error-handling";
32

4-
export class DepositNotFoundException extends HttpError {
3+
export class DepositNotFoundException extends IndexerHTTPError {
54
constructor() {
6-
super("Deposit not found");
7-
this.name = "DepositNotFoundException";
8-
this.status = HttpStatus.NOT_FOUND;
5+
super(
6+
StatusCodes.NOT_FOUND,
7+
DepositNotFoundException.name,
8+
"Deposit not found given the provided constraints",
9+
);
910
}
1011
}
1112

12-
export class IndexParamOutOfRangeException extends HttpError {
13+
export class IndexParamOutOfRangeException extends IndexerHTTPError {
1314
constructor(message: string) {
14-
super(message);
15-
this.name = "IndexParamOutOfRangeException";
16-
this.status = HttpStatus.BAD_REQUEST;
15+
super(StatusCodes.BAD_REQUEST, IndexParamOutOfRangeException.name, message);
1716
}
1817
}

0 commit comments

Comments
 (0)