-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
mrc-6085 Error handling #3
Changes from all commits
0d14148
4d98f83
dd25f57
5894fb9
3bcbe9a
0310015
381611c
dfdf1ec
f6a83f7
2d5c3c0
ab485a9
507c75d
9224ee0
bbb2e95
326efdc
4ad0ca4
cecd991
e278c21
aabe83c
3ea2a0e
fef5ebb
e8491fd
bc88234
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,9 @@ | ||
import { Request, Response } from "express"; | ||
import { jsonResponseSuccess } from "../jsonResponse"; | ||
|
||
export class IndexController { | ||
static getIndex = (_req: Request, res: Response) => { | ||
const version = process.env.npm_package_version; | ||
res.status(200).json({ version }); | ||
jsonResponseSuccess({ version }, res); | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,28 +1,49 @@ | ||
import { Request, Response } from "express"; | ||
import { NextFunction, Request, Response } from "express"; | ||
import { AppLocals } from "../types/app"; | ||
import asyncControllerHandler from "../errors/asyncControllerHandler"; | ||
import notFound from "../errors/notFound"; | ||
import { GroutError } from "../errors/groutError"; | ||
import { ErrorType } from "../errors/errorType"; | ||
|
||
const parseIntParam = (param: string): number => { | ||
// Native parseInt is not strict (ignores whitespace and trailing chars) so test with a regex | ||
if (!/^\d+$/.test(param)) { | ||
throw new GroutError( | ||
`"${param}" is not an integer`, | ||
ErrorType.BAD_REQUEST | ||
); | ||
} | ||
return parseInt(param, 10); | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not use quinary? Decimal is so 2024. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Banking on retro vibes being in next season.
Comment on lines
+8
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. dont have to change this but you could use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What would parseInt(Number(string)) do? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can do const num = Number(string);
if (!Number.IsInteger) {
throw...
}
return num guess its about the same XD up to you There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, I might just leave it as it is. Nice idea though, thanks! |
||
|
||
export class TileController { | ||
static getTile = async (req: Request, res: Response) => { | ||
const { dataset, level, z, x, y } = req.params; | ||
const { tileDatasets } = req.app.locals as AppLocals; | ||
static getTile = async ( | ||
req: Request, | ||
res: Response, | ||
next: NextFunction | ||
) => { | ||
await asyncControllerHandler(next, async () => { | ||
const { dataset, level, z, x, y } = req.params; | ||
const { tileDatasets } = req.app.locals as AppLocals; | ||
|
||
let tileData = null; | ||
if (tileDatasets[dataset] && tileDatasets[dataset][level]) { | ||
const db = tileDatasets[dataset][level]; | ||
tileData = await db.getTileData( | ||
parseInt(z), | ||
parseInt(x), | ||
parseInt(y) | ||
); | ||
} | ||
let tileData = null; | ||
if (tileDatasets[dataset] && tileDatasets[dataset][level]) { | ||
const db = tileDatasets[dataset][level]; | ||
tileData = await db.getTileData( | ||
parseIntParam(z), | ||
parseIntParam(x), | ||
parseIntParam(y) | ||
); | ||
} | ||
|
||
if (tileData) { | ||
res.writeHead(200, { | ||
"Content-Type": "application/octet-stream", | ||
"Content-Encoding": "gzip" | ||
}).end(tileData); | ||
} else { | ||
res.writeHead(404).end(); | ||
} | ||
if (tileData) { | ||
res.writeHead(200, { | ||
"Content-Type": "application/octet-stream", | ||
"Content-Encoding": "gzip" | ||
}).end(tileData); | ||
} else { | ||
notFound(req); | ||
} | ||
}); | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { NextFunction } from "express"; | ||
|
||
// This method should be used to wrap any async controller methods to ensure error handling is applied | ||
export default async (next: NextFunction, method: () => void) => { | ||
try { | ||
await method(); | ||
} catch (error) { | ||
next(error); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do we use next for in this app, if anything? As far as I can tell from reading about it, it's for falling back to other routes when the route has an error? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It hands on to the next middleware which can handle the error. We need it for |
||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
export const enum ErrorType { | ||
BAD_REQUEST = "BAD_REQUEST", | ||
NOT_FOUND = "NOT_FOUND", | ||
UNEXPECTED_ERROR = "UNEXPECTED_ERROR" | ||
} | ||
|
||
export const ErrorTypeStatuses: { [key in ErrorType]: number } = { | ||
[ErrorType.BAD_REQUEST]: 400, | ||
[ErrorType.NOT_FOUND]: 404, | ||
[ErrorType.UNEXPECTED_ERROR]: 500 | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { ErrorType, ErrorTypeStatuses } from "./errorType"; | ||
|
||
export class GroutError extends Error { | ||
errorType: ErrorType; | ||
|
||
constructor(message: string, errorType: ErrorType) { | ||
super(message); | ||
|
||
this.name = "GroutError"; | ||
this.errorType = errorType; | ||
} | ||
|
||
get status() { | ||
return ErrorTypeStatuses[this.errorType]; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { NextFunction, Response } from "express"; | ||
import { uid } from "uid"; | ||
import { GroutError } from "./groutError"; | ||
import { ErrorType } from "./errorType"; | ||
import { RequestWithError } from "../logging"; | ||
import { jsonResponseError } from "../jsonResponse"; | ||
|
||
// We need to include the unused next var for this to be used correctly as an error handler | ||
export const handleError = ( | ||
err: Error, | ||
req: RequestWithError, | ||
res: Response, | ||
_: NextFunction // eslint-disable-line @typescript-eslint/no-unused-vars | ||
) => { | ||
const groutError = err instanceof GroutError; | ||
|
||
const status = groutError ? err.status : 500; | ||
const type = groutError ? err.errorType : ErrorType.UNEXPECTED_ERROR; | ||
|
||
// Do not return raw messages from unexpected errors to the front end | ||
const detail = groutError | ||
? err.message | ||
: `An unexpected error occurred. Please contact support and quote error code ${uid()}`; | ||
|
||
// Set error type, detail and stack on req so morgan logs them | ||
req.errorType = type; | ||
req.errorDetail = detail; | ||
req.errorStack = err.stack; | ||
|
||
jsonResponseError(status, type, detail, res); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { Request } from "express"; | ||
import { ErrorType } from "./errorType"; | ||
import { GroutError } from "./groutError"; | ||
|
||
export default (req: Request) => { | ||
const { url } = req; | ||
throw new GroutError(`Route not found: ${url}`, ErrorType.NOT_FOUND); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { Response } from "express"; | ||
|
||
const addContentType = (res: Response) => { | ||
res.header("Content-Type", "application/json"); | ||
}; | ||
|
||
export const jsonResponseSuccess = (data: object | string, res: Response) => { | ||
addContentType(res); | ||
const responseObject = { | ||
status: "success", | ||
errors: null, | ||
data | ||
}; | ||
res.end(JSON.stringify(responseObject)); | ||
}; | ||
|
||
export const jsonResponseError = ( | ||
httpStatus: number, | ||
error: string, | ||
detail: string, | ||
res: Response | ||
) => { | ||
addContentType(res); | ||
const responseObject = { | ||
status: "failure", | ||
errors: [{ error, detail }], | ||
data: null | ||
}; | ||
res.status(httpStatus); | ||
res.end(JSON.stringify(responseObject)); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,23 @@ | ||
import { Router } from "express"; | ||
import { IndexController } from "./controllers/indexController"; | ||
import { TileController } from "./controllers/tileController"; | ||
import notFound from "./errors/notFound"; | ||
|
||
export const registerRoutes = () => { | ||
const router = Router(); | ||
router.get("/", IndexController.getIndex); | ||
router.get("/tile/:dataset/:level/:z/:x/:y", TileController.getTile); | ||
|
||
// provide an endpoint we can use to test 500 response behaviour by throwing an "unexpected error" - but only if we | ||
// are running in a non-production mode indicated by an env var | ||
if (process.env.GROUT_ERROR_TEST) { | ||
router.get("/error-test", () => { | ||
throw Error("Testing error behaviour"); | ||
}); | ||
} | ||
Comment on lines
+11
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice this is a neat idea, we should definitely use it more! |
||
|
||
// Throw 404 error for any unmatched routes | ||
router.use(notFound); | ||
|
||
return router; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,7 +27,7 @@ | |
</style> | ||
</head> | ||
<body> | ||
<h1>GROUT</h1> | ||
<h1>GROUT Test Page</h1> | ||
<div id="map"></div> | ||
<script> | ||
const map = L.map("map").setView({lon: 0, lat: 0}, 3); | ||
|
@@ -79,7 +79,7 @@ <h1>GROUT</h1> | |
style: { | ||
weight: 1, | ||
fill: false, | ||
color: "#000000" | ||
color: "#777" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tone down garish country outlines on test page! |
||
}, | ||
...options | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would have expected a docker/run script to be used for a deployment. Maybe the env var check in src/routes.ts should also check what environment we're in directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, this is just a dev/CI script, we won't be using it for deployment, that'll be a separate tool. I think it's easier to be explicit when we want the testing route to be added (really just when using this script for integration testing locally/on CI).