Global error handling / customize response json format
marceloverdijk opened this issue · 8 comments
In case of a zod validation error the error response is something like:
{
"errors": [
{
"code": "invalid_type",
"expected": "string",
"received": "array",
"path": [
"query",
"sort"
],
"message": "Expected string, received array"
}
],
"success": false,
"result": {}
}
I want to change this error format globally (for all routers) so it is uses the JSON:API specification for errors:
{
"errors": [
{
"status": "422",
"title": "invalid_type",
"detail": "Expected string, received array."
"meta": {
"code": "invalid_type",
"expected": "string",
"received": "array",
"path": [
"query",
"sort"
],
"message": "Expected string, received array"
},
}
]
}
What is the best way to solve this?
Besides zod errors, my routers may call service class that throw errors as well.
I want to handle the globally, and create a similar response.
I'm missing any relevant information about how to do such (global) error handling.
I've used the itty-router catch
option to configure a custom error handler like:
const router = Router({
catch: errorHandler,
});
this works for custom exception/error thrown from my services classes.
But, this does not allow me to customize the zod validation error responses unfortunately.
@G4brym any recommendations on this?
PS: I went now for this quick hack'ish solution:
OpenAPIRoute.prototype.handleValidationError = function(errors: z.ZodIssue[]): Response {
throw new z.ZodError(errors);
};
const router = Router({
catch: errorHandler,
});
and in that error handler, besides my custom errors, I also handle the zod errors to render the expected error format for my application.
I think it would nice if Chanfana would provide a custom handleValidationError
router option to customize the zod validation error responses.
Hey @marceloverdijk I just wrote a quick page on how to handle validation errors across multiple endpoints.
I think making use of JS classes is better than defining a global error handler in the router, this way the developer can have multiple error handlers and choose to extend just the desired endpoints
let me know what you think
https://chanfana.pages.dev/advanced-user-guide/custom-error-handling/
Thx, yes extending OpenAPIRoute
was my other option. Thx for sharing!
Hi @G4brym ,
While extending OpenAPIRoute
and looking at the route.ts source I noticed:
getSchema(): OpenAPIRouteSchema {
// Use this function to overwrite schema properties
return this.schema
}
Which gave me some idea ;-)
As I have a fair amount of endpoints having paging params (page
and page_size
) I thought I could maybe create a MyPagingRoute
and add these params to the schema automatically.
And define a applyQueryParams
to this class to call it from handle
implementations.
However, I noticed these paging params will then not be part of the the validated schema data:
const data = await this.getValidatedData<typeof this.schema>();
const page = data.query.page // or data.query[QueryParam.PAGE];
So maybe my approach is not recommended at all ;-)
I'm wondering if you are using similar patterns like this at Cloudflare or not?
My alternative is to simply abandon my MyPagingRoute
idea and have some re-usable shema objects directly in the OpenAPIRouteSchema.schema
definition.
export class MyPagingRoute extends MyProjectRoute {
defaultPageSize = 10;
maxPageSize = 50;
getSchema(): OpenAPIRouteSchema {
extendQuerySchema(this.schema, QueryParam.PAGE, z.number().int().optional());
extendQuerySchema(this.schema, QueryParam.PAGE_SIZE, z.number().int().max(this.maxPageSize).optional());
return super.getSchema();
}
applyQueryParams(qb: SQLiteSelect): void {
// get page and page_size from validated data or directly from raw request?
// how to do this best?
}
}
const extendQuerySchema = (schema: OpenAPIRouteSchema, field: QueryParam, augmentation: ZodTypeAny) => {
// Ensure request and request.query schema are defined.
schema.request = schema.request || {};
schema.request.query = schema.request.query || z.object({});
const queryShape = schema.request.query.shape;
// Extend schema (only if field is not present).
if (!(field in queryShape)) {
schema.request.query = schema.request.query.extend({ [field]: augmentation });
}
};
I have a bunch of these kinds of parameter injections, like removing debug parameters in production envs, etc
You need to overwrite the getSchemaZod
function, this way the new parameters will be part of the validation schema.
This example should work, you can also safely call multiple times the this.getValidatedData()
as it will only be executed once
export class MyPagingRoute extends MyProjectRoute {
defaultPageSize = 10;
maxPageSize = 50;
getSchemaZod(): OpenAPIRouteSchema {
// Deep copy
const schema = { ...this.getSchema() }
schema.request.query = schema.request.query.merge(z.object({
page: z.number().int().optional()
page_size: z.number().int().max(this.maxPageSize).optional()
}))
// @ts-ignore
return schema
}
applyQueryParams(qb: SQLiteSelect): void {
const validatedData = await this.getValidatedData()
// do something with page and size
}
}
Edit:
Actually, overwriting the getSchema
should also have worked, maybe it didn't because you tried to update this.schema
direcly without doing a deep copy of the object
Hi @G4brym thx for your reply.
I tried with both overriding getSchemaZod()
and getSchema()
.
And it gets the data with the following code:
but it complains query could be undefined
.
so this way I seem to lose all type safety... is that expected with this approach?