-
Notifications
You must be signed in to change notification settings - Fork 535
Queries returning only errors are forced to be a 500 #427
Comments
+1 having an issue doing proper error handling in client-side as it always returns status 500 if the response must be non null. |
@andfk Can you elaborate on your solution? What version are you using now? How did it change? Best! |
hey @masiamj yeah. I think i may have to edit my comment as is wrong. I initially thought the error was coming from Apollo and it'll be solved by upgrading Hope that helps and lets see how this issue goes. I personally like much more the previous approach. |
I'm facing the same issue. As @derek-miller said, it's caused by these lines in the aforementioned merge request:
I'd love to have a possibility to change this behavior. |
+1 |
Hardcoded 5xx errors made me a little sad, as this might confuse certain GraphQL clients. This PR doesn't seem to be exhaustive or about to be merged and I also didn't feel like maintaining a fork. I therefore resorted to the next best thing, hijacking the express send handler. 🐴 import { NextFunction, Request, Response } from "express";
import * as hijackResponse from "hijackresponse";
// Extend Express Response with hijack specific function
interface IHijackedResponse extends Response {
unhijack: () => void;
}
/**
* Stupid problems sometimes require stupid solutions.
* Unfortunately `express-graphql` has hardcoded 4xx/5xx http status codes in certain error scenarios.
* In addition they also finalize the response, so no other middleware shall prevail in their wake.
*
* It's best practice to always return 200 in GraphQL APIs and specify the error in the response,
* as otherwise clients might choke on the response or unnecessarily retry stuff.
* Also monitoring is improved by only throwing 5xx responses on unexpected server errors.
*
* This middleware will hijack the `res.send` method which gives us one last chance to modify
* the response and normalize the response status codes.
*
* The only alternative to this would be to either fork or ditch `express-graphql`. ;-)
*/
export const responseHijack = (_: Request, originalRes: Response, next: NextFunction) => {
hijackResponse(originalRes, (err: Error, res: IHijackedResponse) => {
// In case we encounter a "real" non GraphQL server error we keep it untouched and move on.
if (err) {
res.unhijack();
return next(err);
}
// We like our status code simple in GraphQL land
// e.g. Apollo clients will retry on 5xx despite potentially not necessary.
res.statusCode = 200;
res.pipe(res);
});
// next() must be called explicitly, even when hijacking the response:
next();
}; Usage: import { responseHijack } from "./expressMiddleware/responseHijack";
app.use(responseHijack); Please note: My inline comment is not meant to be snarky or condescending, I appreciate all open source work ❤️ |
If resolver returns only errors its incorrect set status to 500, it's may be bad request or forbidden etc |
Any update on this? Validation errors should definitely not be returning a 500. |
The lines before the one that sets 500: .catch(error => {
// If an error was caught, report the httpError status, or 500.
response.statusCode = error.status || 500;
return { errors: [error] };
}) So if you add an extension that examines the Also note that in an extension, the errors are GraphQL errors and they have an |
@robatwilliams That kinda works, though the Typescript typings require the |
Implementation is fine with returning non-object, so typings need updating: if (extensions && typeof extensions === 'object') {
(result: any).extensions = extensions;
} Yes, the single thrown error will replace any existing errors as I said. Agree it's not ideal, the approach might work for some. |
I think there should be a hook that we can add, similar to |
+1 |
Still waiting for a fix on this.. |
I've made a simple rewriter for my app using on-headers package. const onHeaders = require('on-headers');
function graphqlStatusCodeRewriter(req, res, next) {
const handleHeaders = () => {
res.statusCode = res.statusCode === 500 ? 400 : res.statusCode;
};
onHeaders(res, handleHeaders);
next();
};
// Include before your express-graphql middleware
app.use('/graphql', graphqlStatusCodeRewriter);
app.use('/graphql', graphqlHTTP({ schema })); When it encounters a |
@coockoo I guess that might work if all of your data is static and in-memory, but if there's anything where an actual internal error can occur, you'll just be signalling to your consumers that it's their fault, not a (hopefully temporary) issue on your end. This would be pretty confusing. I think you need the actual error to be able to handle it properly, like the hijack response approach. |
@seeruk As for now, I can see that And of course, all of these solutions are temporary and must be replaced with possibility to customize status codes by |
Any guidance here? I would be happy to make a PR to either:
Option #2 is how the other graphql servers I've worked with do it. Either option is preferable to hard coded 500. |
completely agreeing with @berstend on this:
While I understand that It seems like there has been no real progress on this issue. Given the ~5.3k ⭐️ marks on this project, I (hopefully with a bunch of other people as well) would like to understand if this issue is up for a fix consideration, or whether alternative solutions should be sought. And, while I'm here - thx for creating and maintaining this!! OSS can be a true PITA, and I appreciate every damn minute you folks are putting into this. Keep up the good work, and LMK if help would be appreciated with this one |
my typescript solution / workaround: app.post('/',
jwtAuth,
graphqlHTTPOptions200,
graphqlHTTPError200,
graphqlHTTP({
schema: makeExecutableSchema({typeDefs: [DIRECTIVES, SCHEMEA], resolvers: schemaResolvers}),
graphiql: false,
})) function graphqlHTTPError200(request: Request, response: Response, next: NextFunction): void
{
const defaultWrite = response.write
const defaultEnd = response.end
const defaultWriteHead = response.writeHead
const chunks: any[] = []
let isGqlError: boolean = false
response.write = (...chunk: any): any =>
{
chunks.push(Buffer.from(chunk[0]))
defaultWrite.apply(response, chunk)
}
response.end = (...chunk: any) =>
{
if (chunk[0]) chunks.push(Buffer.from(chunk[0]))
isGqlError = !!Buffer.concat(chunks).toString('utf8').match(/"errors":\[/)
defaultEnd.apply(response, chunk)
}
response.writeHead = (statusCode: number) =>
{
return defaultWriteHead.apply(response, isGqlError ? [200] : [statusCode])
}
next()
} |
I have opened a PR, #696, to address this issue. Any feedback is welcome. |
@proehlen the best place for this discussion would be: this is where the whole spec for HTTP error codes is decided on. if the HTTP spec changes, we can update this reference implementation! |
The question I have is how #696 violates the spec? |
@MatthiasKunnen this was @IvanGoncharov's resolution on slack:
|
@acao thanks for the link. I don't feel confident enought to raise a new issue there. I've hacked around it using @crazyx13th 's solution in the mean time but if I could just make a couple of observations here before I move on: In general, I'm not sure http status code is an approriate mechanism for indicating problems with queries that managed to resolve (even if aborted by the api for whatever reason). For one thing, the query can have nested/multiple errors and be partially successful, partially unsucessful. One blanket http status code doesn't really cover the multitude of scenarios. Also, 500 is obviously not appropriate for many or even most errors, causes problems for some users, and the 500 code itself specifically is not actually prescribed by the spec. Pull request #696 would have allowed me to raise a custom error object with a status code in my api and then set the http response accordingly. It would have then been my responsibility to ensure I was compliant with the spec - ie returning a 4xx or 5xx status as appropriate. Thanks again for your time. |
I agree with @proehlen, I also wished to use |
personally, I think it makes sense too. we can improve the HTTP transport spec error codes as much as we want, but users will almost always have edge cases or different needs altogether. following the spec by default is good enough for me and for a reference implementation, and it doesn't add any performance debt or almost any maintenance burden to add this. @danielrearden what is your take on this? @IvanGoncharov are you willing to revisit this decision? |
I think it should be revisited and implemented in such a way that it cannot violate the spec if desired. If the change were to supply a function that returns the status code in error situations it could enforce spec compliance vs the custom response function proposed. As shown above, you can always hack around it and violate the spec. The library should do its best to maintain compliance while not blocking the user. Returning 500 on any error is arguably worse than violating a spec imo. |
Forcing spec compliance could be done but I don't see much advantage in that. After all, as you said, you can just hack your way around it. IMO, the most important thing is that the default setting does not violate the spec. What the user then decides to do with the function is their business. |
💯. this solves all the issues we’re trying to solve here. it works as the user should expect. this is what we’re already doing with to limit support requests when people diverge from spec, we can add a “use at your own risk” warning perhaps? |
We could make the description as follows:
|
You can return 400 via import { execute } from 'graphql';
graphqlHTTP({
async customExecuteFn(args) {
const result = await execute(args);
if (result.errors?.[0]) throw result.errors[0];
return result;
}
}) If my method is correct, I hope to provide similar examples in the documentation. |
@mantou132 I don't know why what you have is returning a 400... but it does! Its better than a 500 but seems to still lack specificity. In my opinion you should just be looking on the error objects themselves and the first one with a class UnauthorizedError extends Error {
public statusCode = 401
constructor() {
super("Unauthorized")
}
} // my @auth directive
if (!context.user) throw new UnauthorizedError()
return next(root, args, context, info) By all means return a 200 automatically on success but you need to just give me a function to handle the error and pick the statusCode. It makes no sense to return a 500 in any case other than the default case where you have no idea what to do, even then arguably it should be a 400. Otherwise give me a function and let me figure it out, you'll never be able to just blindly make a one-size-fits-all solution. |
I've noticed that when I return an error for a query the status is I'm not sure why that is but it seems related to this discussion. I'm undecided on what I want it to do, but would appreciate either consistency or a way to customize it. |
I believe the authors of the library are misreading the spec:
My response contains a null data entry as described, and yet it still corrupts the status code. |
The spec seems pretty clear to me, but it was updated a couple of months ago so I guess it has changed since the maintainers reverted the PR. It looks like the current behaviour, while correct at the time, is now wrong:
I modified @mantou132's solution to get it to return HTTP 200 all the time, as the Apollo GraphQL client won't see the error messages unless a HTTP 200 is returned. (Per the spec, 400/500 responses may not be JSON so it only parses 200 responses.) This is what worked for me:
|
This library has been deprecated and this repo will be archived soon. It has been superseded by Furthermore, if you seek a fully-featured, well-maintained and performant server - I heavily recommend GraphQL Yoga! |
On commit b3ccce9 a query response containing only the
errors
field is forced to be a 500, however these errors could easily be coercion errors due to incorrect input types, thus a bad request and not a server error. I am wondering if there is a strong opinion to keep it this way or if we could improve this logic and potentially make it configurable? A lot of client code that makes graphQL requests often retry on 500 errors and in the case of coercion errors will never succeed.The text was updated successfully, but these errors were encountered: