Skip to content
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

Merged
merged 23 commits into from
Dec 19, 2024
Merged

mrc-6085 Error handling #3

merged 23 commits into from
Dec 19, 2024

Conversation

EmmaLRussell
Copy link
Contributor

@EmmaLRussell EmmaLRussell commented Dec 9, 2024

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 by handleError 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 of GroutError 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 by morgan. This means that we can find the stack corresponding to an unexpected error code in the logs if required.
  • asyncControllerHandler - this allows handleError to be triggered when the error is thrown asynchronously to the initial request call. It does this by catching any error, and ensuring that next is called with the error as parameter. This handler should be used by any controller method which is async.
  • notFound - utility function for throwing a GroutError for any 404 scenario. Used by both the router and the TileController (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 to http://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 the grout 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 to http://localhost:5000/error-test should return a 404 rather than a 500.

@EmmaLRussell EmmaLRussell changed the base branch from main to mrc-6064-serve-tile-data December 9, 2024 16:49
@@ -79,7 +79,7 @@ <h1>GROUT</h1>
style: {
weight: 1,
fill: false,
color: "#000000"
color: "#777"
Copy link
Contributor Author

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!

@EmmaLRussell EmmaLRussell marked this pull request as ready for review December 17, 2024 14:56
Copy link
Contributor

@M-Kusumgar M-Kusumgar left a 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

status: number;
errorType: ErrorType;

constructor(message: string, status: number, errorType: ErrorType) {
Copy link
Contributor

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

Copy link
Contributor Author

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
Comment on lines 5 to 11
interface RequestWithError {
errorType: string;
errorDetail: string;
errorStack: string | undefined;
}
export const reqWithError = (req: Request) =>
req as unknown as RequestWithError;
Copy link
Contributor

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:

  1. req is not actually just errorType, errorDetail, errorStack, its much more
  2. 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

Copy link
Contributor Author

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
Comment on lines 37 to 39
tokens["error-type"](req, res),
tokens["error-detail"](req, res),
tokens["error-stack"](req, res)
Copy link
Contributor

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

Comment on lines +11 to +17
// 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");
});
}
Copy link
Contributor

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!

@M-Kusumgar
Copy link
Contributor

M-Kusumgar commented Dec 18, 2024

Also side note, i could not get the docker/run script to run, i think it is because the wrong tag is pushed on github actions, i believe you cant just do git rev-parse to get the sha on github actions because it does a new merge commit or something like that, you can bake it into github actions, i did this for hint here https://github.com/hivtools/hint/blob/73f5992bdc9d0dee0801a0764f930b8737819255/.github/actions/make-shared-env/action.yml#L39

alternatively to get it working for now i just changed $TAG_SHA to $TAG_BRANCH in docker/run because it was pushed with the correct branchname

@EmmaLRussell
Copy link
Contributor Author

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!

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...

@EmmaLRussell
Copy link
Contributor Author

Also side note, i could not get the docker/run script to run, i think it is because the wrong tag is pushed on github actions, i believe you cant just do git rev-parse to get the sha on github actions because it does a new merge commit or something like that, you can bake it into github actions, i did this for hint here https://github.com/hivtools/hint/blob/73f5992bdc9d0dee0801a0764f930b8737819255/.github/actions/make-shared-env/action.yml#L39

alternatively to get it working for now i just changed $TAG_SHA to $TAG_BRANCH in docker/run because it was pushed with the correct branchname

oops! Good catch. We definitely want to use the sha though, as ./docker/run is used for the integration tests in CI before it pushes the branch tag. Will use your trick.

@david-mears-2
Copy link
Contributor

It is just a test page, guys...

For this reason, I would put some notice on the page saying 'test page' to head off further UI comments

@EmmaLRussell
Copy link
Contributor Author

It is just a test page, guys...

For this reason, I would put some notice on the page saying 'test page' to head off further UI comments

<h1>Deliberately Ugly Test Page</h1>
<p>If you can see the ugly, it means it's working</p> 

@@ -7,5 +7,6 @@ docker run -d --rm \
--pull=always \
-p 5000:5000 \
-v ./data:/data \
-e GROUT_ERROR_TEST=true \
Copy link
Contributor

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.

Copy link
Contributor Author

@EmmaLRussell EmmaLRussell Dec 18, 2024

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);
Copy link
Contributor

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.

Copy link
Contributor Author

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);
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Comment on lines 4 to 5
status: number;
errorType: ErrorType;
Copy link
Contributor

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?

Copy link
Contributor

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)

Copy link
Contributor Author

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.

@EmmaLRussell EmmaLRussell changed the base branch from mrc-6064-serve-tile-data to main December 19, 2024 10:05
Copy link
Contributor

@M-Kusumgar M-Kusumgar left a 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!

Comment on lines +8 to +16
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);
Copy link
Contributor

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

Copy link
Contributor Author

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?

Copy link
Contributor

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?

Copy link
Contributor

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

Copy link
Contributor Author

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!

Copy link

codecov bot commented Dec 19, 2024

Codecov Report

Attention: Patch coverage is 97.01493% with 2 lines in your changes missing coverage. Please review.

Project coverage is 98.27%. Comparing base (77cac05) to head (bc88234).
Report is 24 commits behind head on main.

Files with missing lines Patch % Lines
src/routes.ts 50.00% 2 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

@EmmaLRussell
Copy link
Contributor Author

Disregarding the codecov grumble!

@EmmaLRussell EmmaLRussell merged commit b4c775a into main Dec 19, 2024
2 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants