Tom
Tom12mo ago

working with custom errors and trpc errorFormatter

Hey guys. I'm trying to refactor my app so that all errors extend a BaseError class to make dealing with error codes and user-friendly messages easier. I'm having trouble converting these to the correct shape in the tRPC errorFormatter. Right now when I throw these custom errors tRPC doesnt recognize them and converts all of them to INTERNAL_SERVER_ERROR's. I see that some error codes get translated in 3 places: the jsonrpc error code, the http status code, and an HTTP status,. There also seems to be some additional structure. My end goal is to be able to convert my errors to the 'normal' shape so that trpc-openapi can correctly return them. Is there a reasonable way to do this in tRPC or should I do something different? Its easy enough for me to convert one of my errors to a TRPCError, but I don't see a way to convert that to the right shape once I'm in the errorFormatter. is there a way to do this?
5 Replies
Nick
Nick12mo ago
It should pass through your errorFormatter and you can detect it and decide what to do with it. You probably want the error.cause property to access your thrown error Try just console logging everything out to start and then go from there
Tom
Tom12mo ago
I got that far, but it seems like I need to be able to get a few fields that i dont really know how to generate: 1) shape.code (which looks to be some kind of JSONRPC code?) 2) shape.data httpStatus (is there an easy way to get this from a TRPCError code?) 3) can i add an additional customAppCode somewhere in this structure? do i put it in the root of the object or shape.data? I'm not really sure what the implications of any of this are. thanks for any help Ok so i did some more looking into this and now im more confused after some confusing results I added this to my errorFormatter():
errorFormatter(params) {
console.log("------------------------------");
console.log(params.shape);
console.log("------------------------------");

return {
message: "test",
code: -32004,
data: {
code: "NOT_FOUND",
httpStatus: 404,
stack: params.shape.data.stack,
path: params.shape.data.path,
},
};
}
errorFormatter(params) {
console.log("------------------------------");
console.log(params.shape);
console.log("------------------------------");

return {
message: "test",
code: -32004,
data: {
code: "NOT_FOUND",
httpStatus: 404,
stack: params.shape.data.stack,
path: params.shape.data.path,
},
};
}
this data exactly matches the data i get when i throw a regular TRPCError this seems to work in regular trpc (although im still not sure how i should be generating all these codes) but in trpc-openapi i always get a 500 if the original error isnt a TRPCError ok so after a bunch of trying things im most of the way there with this code:
errorFormatter(params) {
if (params.error.cause && params.error.cause instanceof BaseError) {
const httpCode = params.error.cause.getHttpStatusCode();

return {
message: params.error.cause.displayMessage ?? "Internal Server Error",
code: getJsonRpcErrorCode(httpCode),
data: {
code: params.error.cause.code,
httpStatus: httpCode,
path: params.shape.data.path,
stack: params.shape.data.stack,
},
};
} else {
return {
message: "Internal Server Error",
code: getJsonRpcErrorCode(500),
data: {
code: params.error.code,
httpStatus: 500,
path: params.shape.data.path,
stack: params.shape.data.path,
},
};
}
},
errorFormatter(params) {
if (params.error.cause && params.error.cause instanceof BaseError) {
const httpCode = params.error.cause.getHttpStatusCode();

return {
message: params.error.cause.displayMessage ?? "Internal Server Error",
code: getJsonRpcErrorCode(httpCode),
data: {
code: params.error.cause.code,
httpStatus: httpCode,
path: params.shape.data.path,
stack: params.shape.data.stack,
},
};
} else {
return {
message: "Internal Server Error",
code: getJsonRpcErrorCode(500),
data: {
code: params.error.code,
httpStatus: 500,
path: params.shape.data.path,
stack: params.shape.data.path,
},
};
}
},
but trpc-openapi doesnt completely work. I can change the http status code with this responseMeta() call:
responseMeta: (opts) => {
if (opts.errors[0]?.cause instanceof BaseError) {
return { status: opts.errors[0].cause.getHttpStatusCode() };
} else {
return { status: 500 };
}
},
responseMeta: (opts) => {
if (opts.errors[0]?.cause instanceof BaseError) {
return { status: opts.errors[0].cause.getHttpStatusCode() };
} else {
return { status: 500 };
}
},
but this still doesnt change the code that is actually sent down in the body:
curl -v localhost:3000/api/v1/test
* Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET /api/v1/test HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/8.1.2
> Accept: */*
>
< HTTP/1.1 404 Not Found # <------------------------------------------ CORRECT ERROR CODE FROM APP
< Access-Control-Allow-Origin: *
< Content-Type: application/json
< Vary: Accept-Encoding
< Date: Thu, 19 Oct 2023 06:07:18 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
{"message":"test","code":"INTERNAL_SERVER_ERROR"}% # <----------------- ALWAYS INTERNAL_SERVER_ERROR
curl -v localhost:3000/api/v1/test
* Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET /api/v1/test HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/8.1.2
> Accept: */*
>
< HTTP/1.1 404 Not Found # <------------------------------------------ CORRECT ERROR CODE FROM APP
< Access-Control-Allow-Origin: *
< Content-Type: application/json
< Vary: Accept-Encoding
< Date: Thu, 19 Oct 2023 06:07:18 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
{"message":"test","code":"INTERNAL_SERVER_ERROR"}% # <----------------- ALWAYS INTERNAL_SERVER_ERROR
it seems that the TRPCError.code variable is also readonly, do i cant change it im also not sure if im handling stack correctly. will it still properly get omitted in production builds?
Nick
Nick12mo ago
It's really up to you how you use the errorFormatter, but I would avoid thinking in HTTP. This isn't REST, the whole point is to think in Typescript, so craft types that represent your errors and return them, then they'll be inferred and typesafe on the frontend I'm not really very familiar with trpc-openapi though, it's not a project I'm involved in or have used
Tom
Tom12mo ago
I'm not trying to think in HTTP. openapi-trpc should do that for me (at least thats the theory) the problem im having is that i cant figure out how to take a non-trpc error in the error formatter and make it the same shape as a native trpc error if i only ever threw TRPCErrors then everything would work fine (which is what my app has been doing up to this point) but even if i know all the parameters that i would pass to a TRPCError, once its in the error formatter it seems that I lose the ability to replicate the same exact shape i guess a simpler, but still practical, example would be like lets say I have TRPCError({message: "You aren't allowed to see this", code: 'UNAUTHORIZED'}) but then i want to enforce a rule in my error formatter that says 'for security reasons, i dont want to return UNAUTHORIZED because that tells the client that this resource exists. I want to convert this to NOT_FOUND' as far as i can tell, i cant really do that is that accurate?
Nick
Nick12mo ago
You don’t need to match the shape of the trpc error, there are a few keys that tRPC requests in the formatter that you can just copy over, but then you attach what you like and the frontend will get access There’s no need to throw any specific error class yourself, it’s totally fine to use custom ones