-
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
Conversation
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
Tone down garish country outlines on test page!
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.
this is awesome! love the high res data! i will say i think the outlines are too thick at zoomed out levels XD and tooltips stay on when i move my mouse to water, but obviously in development, excited to use this at some point!
I have a couple typescript suggestions but other than that have checked out the endpoints, works well
src/errors/groutError.ts
Outdated
status: number; | ||
errorType: ErrorType; | ||
|
||
constructor(message: string, status: number, errorType: ErrorType) { |
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.
do we expect the error types enum to not have a 1 to 1 mapping with the status number? because if so we can just derive the status from the error type enum with a mapping or just changing the enum to reflect that feels easy to remove extra arg
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 think at the moment that's true, we're kind of hedging our bets here. Yeah, I'll take it out, we can add it back if we need it.
src/logging.ts
Outdated
interface RequestWithError { | ||
errorType: string; | ||
errorDetail: string; | ||
errorStack: string | undefined; | ||
} | ||
export const reqWithError = (req: Request) => | ||
req as unknown as RequestWithError; |
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.
hmm i think this is quite weird for 2 reasons:
- req is not actually just
errorType
,errorDetail
,errorStack
, its much more - using a js function to only satisfy typescript seems weird too, we would have to wrap req with this function every time we want these properties and have it unwrapped in the same block of code where we want to use other properties in
Request
type
you can just define request with error type as how it is
export type RequestWithError = Request & {
errorType?: string;
errorDetail?: string;
errorStack?: string;
};
where Request
is the type express exports. Then wherever you use just the request type in function args i.e. arg handleError
just replace req: Request
with req: RequestWithError
, and same thing in all the moken.token
callbacks below
that should solve it without you having to wrap req everytime and it will be typed correctly
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.
You're completely right, I'd just blindly copied this from somewhere else (wodin?) - no idea why it had ended up that way there, maybe some no longer relevant history. Yeah, will tidy that up.
src/logging.ts
Outdated
tokens["error-type"](req, res), | ||
tokens["error-detail"](req, res), | ||
tokens["error-stack"](req, res) |
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.
none of these actually take res
, minor point obviously but could just make res
optional arg in the type and remove res
from here although no harm in leaving this in either
// 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"); | ||
}); | ||
} |
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.
nice this is a neat idea, we should definitely use it more!
Also side note, i could not get the alternatively to get it working for now i just changed |
It is just a test page, guys... I'll maybe spruce it up in a separate ticket, make the outlines thinner when zoomed out and tidy up the tooltips... |
oops! Good catch. We definitely want to use the sha though, as |
For this reason, I would put some notice on the page saying 'test page' to head off further UI comments |
|
@@ -7,5 +7,6 @@ docker run -d --rm \ | |||
--pull=always \ | |||
-p 5000:5000 \ | |||
-v ./data:/data \ | |||
-e GROUT_ERROR_TEST=true \ |
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).
ErrorType.BAD_REQUEST | ||
); | ||
} | ||
return parseInt(param, 10); |
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.
Why not use quinary? Decimal is so 2024.
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.
Banking on retro vibes being in next season.
try { | ||
await method(); | ||
} catch (error) { | ||
next(error); |
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.
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 comment
The 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 handleError
to be triggered.
src/errors/groutError.ts
Outdated
status: number; | ||
errorType: ErrorType; |
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.
Do we need to store both of these? As Mantra said they have a 1-1 mapping?
Or is it that the status can be any valid HTTP status, not just a defined error type status?
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.
Maybe e.g. status should be a getter for the error type, or vice versa. (rather than a property assigned by the constructor)
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 do think they're a bit different conceptually, and I think it's ok for GroutError to manage that. There may come a time when it's not a 1-1 mapping, though it is at the moment. OK, will change to getter as that will cover both cases.
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.
nice, have a small optional comment that might make it a bit simpler but otherwise happy to approve!
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 comment
The reason will be displayed to describe this comment to others. Learn more.
dont have to change this but you could use Number(string)
which does everything you want without you having to write regex, Number
evals the whole string vs parseInt
stopping
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 don't think Number(string)
will help me exclude non-integer numbers though, will it?
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.
What would parseInt(Number(string)) do?
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.
you can do Number.isInteger(Number(string))
for that, so something like
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 comment
The 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!
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3 +/- ##
===========================================
- Coverage 100.00% 98.27% -1.73%
===========================================
Files 7 13 +6
Lines 61 116 +55
Branches 7 15 +8
===========================================
+ Hits 61 114 +53
- Misses 0 2 +2 ☔ View full report in Codecov by Sentry. |
Disregarding the codecov grumble! |
NB based on mrc-6064-serve-tile-data
This branch implements an error handling framework. This is based on the framework implemented in WODIN which seems to work quite well. The idea is that any requests which cannot be completed successfully should cause an Error to be thrown. This Error will be dealt with by the
handleError
method which will ensure that the appropriate JSON error response and status code are returned.Key changes:
GroutError
class. This is for errors which we throw deliberately e.g. in the case of a bad request, or unknown resource requests. Parameters to this class include message, ErrorType and status code. These parameters are used byhandleError
to build the error Response.handleError
- given a thrown error, builds a "Porcelain-style" response with error, status code and error type. If the error is an instance ofGroutError
its message is returned in the response. If not, then this is an unexpected error, and we don't want to return its raw error message to the client. so we include a generic message, along with a unique error code.handleError
includes message (including error code), type and stack in tokens to be logged bymorgan
. This means that we can find the stack corresponding to an unexpected error code in the logs if required.asyncControllerHandler
- this allowshandleError
to be triggered when the error is thrown asynchronously to the initial request call. It does this by catching any error, and ensuring thatnext
is called with the error as parameter. This handler should be used by any controller method which isasync
.notFound
- utility function for throwing a GroutError for any 404 scenario. Used by both the router and theTileController
(for the case where the tile request was for unknown data).jsonResponse
utility for returning JSON error and success responses in our standard Porcelain format. Update to the index response to use this.In order to test behaviour when a 500 error is thrown, I've also added an "/error-test" endpoint to
routes
which is only added if GROUT_ERROR_TEST environment variable is set. This means that we can throw an "expected unexpected" error for testing without needing to include it in the production server. The./docker/run
script sets this env var.You should see that if you run the app using
./docker/run
then browsing tohttp://localhost:5000/error-test
will show a 500 "UNEXPECTED_ERROR" response with an error code, and this should also show up in the logs of thegrout
docker container. The raw error message "Testing error behaviour" should be logged in the container, but not returned in the response.However, if you run the app outside docker with
npm run dev
, the env var will not be set, the route should not be added, and browsing tohttp://localhost:5000/error-test
should return a 404 rather than a 500.