Skip to content
This repository was archived by the owner on Mar 20, 2023. It is now read-only.

Queries returning only errors are forced to be a 500 #427

Open
derek-miller opened this issue Apr 13, 2018 · 38 comments · Fixed by #696 · May be fixed by #780
Open

Queries returning only errors are forced to be a 500 #427

derek-miller opened this issue Apr 13, 2018 · 38 comments · Fixed by #696 · May be fixed by #780

Comments

@derek-miller
Copy link

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.

@andfk
Copy link

andfk commented Apr 24, 2018

+1 having an issue doing proper error handling in client-side as it always returns status 500 if the response must be non null.

@masiamj
Copy link

masiamj commented May 1, 2018

@andfk Can you elaborate on your solution? What version are you using now? How did it change?

Best!

@andfk
Copy link

andfk commented May 1, 2018

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 apollo-link-http anyway the issue still remains. What i ended doing as a temporal solution (i hope) was to remove the ! or new GraphQLNonNull() from the responses so the 500 is not returned if the response is empty when it has errors. For example a user error we throw manually not an unhandled expection or so.

Hope that helps and lets see how this issue goes. I personally like much more the previous approach.

@julkwiec
Copy link

julkwiec commented Sep 14, 2018

I'm facing the same issue. As @derek-miller said, it's caused by these lines in the aforementioned merge request:

        if (response.statusCode === 200 && result && !result.data) {
          response.statusCode = 500;
        }

I'd love to have a possibility to change this behavior.

@ghost
Copy link

ghost commented Dec 13, 2018

+1

@berstend
Copy link

berstend commented Jan 4, 2019

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 ❤️

@deerares
Copy link

If resolver returns only errors its incorrect set status to 500, it's may be bad request or forbidden etc

@jsonmaur
Copy link

Any update on this? Validation errors should definitely not be returning a 500.

@robatwilliams
Copy link

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 result for errors, you can throw an error with a status property which will then be used as the response code. It will replace an errors currently in result.errors.

Also note that in an extension, the errors are GraphQL errors and they have an originalError property.

@jsonmaur
Copy link

@robatwilliams That kinda works, though the Typescript typings require the extensions function to return an object. Which means all responses will have an empty extensions object now. Also, it doesn't allow you to throw multiple errors if you have them, since that catch statement is assuming only one error.

@robatwilliams
Copy link

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.

@seeruk
Copy link

seeruk commented Nov 13, 2019

I think there should be a hook that we can add, similar to customFormatErrorFn. The main thing I wanted to do was log errors on my server, and it's not possible to do that without either ditching express-graphql or using something like hijackresponse. Not ideal workarounds, and it makes it less than ideal to use express-graphql in production.

@Aligertor
Copy link

+1

@heyaco
Copy link

heyaco commented Dec 18, 2019

Still waiting for a fix on this..

@coockoo
Copy link

coockoo commented Dec 22, 2019

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 500 error it replaces it with 400.

@seeruk
Copy link

seeruk commented Dec 22, 2019

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

@coockoo
Copy link

coockoo commented Dec 23, 2019

@seeruk As for now, I can see that hijackresponse calls the callback only in one case with the null as an error, so it doesn't really solve this problem, as if (err) always returns false.

And of course, all of these solutions are temporary and must be replaced with possibility to customize status codes by express-graphql itself.

@timscott
Copy link

Any guidance here? I would be happy to make a PR to either:

  1. Look for statusCode on the error (which can be added in formatError)
  2. Hard code resolver errors as 200, and let extensions tell the client what kind of error(s) happened

Option #2 is how the other graphql servers I've worked with do it. Either option is preferable to hard coded 500.

@DethAriel
Copy link

completely agreeing with @berstend on this:

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.

While I understand that express-graphql might choose to follow a different paradigm, I am a strong believer that supporting industry best practices is beneficial to the ecosystem.

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

@crazyx13th
Copy link

crazyx13th commented Sep 30, 2020

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()
}

@MatthiasKunnen
Copy link
Contributor

I have opened a PR, #696, to address this issue. Any feedback is welcome.

@acao acao closed this as completed in #696 Dec 8, 2020
@proehlen
Copy link

@acao Since pull request #696 has been reverted in #736, can this issue be re-opened? I could log a new issue but the discussion here is useful history.

@acao
Copy link
Member

acao commented Jan 24, 2021

@proehlen the best place for this discussion would be:
https://github.com/graphql/graphql-over-http

this is where the whole spec for HTTP error codes is decided on. if the HTTP spec changes, we can update this reference implementation!

@MatthiasKunnen
Copy link
Contributor

The question I have is how #696 violates the spec?

@acao
Copy link
Member

acao commented Jan 24, 2021

@MatthiasKunnen this was @IvanGoncharov's resolution on slack:

I disagree with 43ba606
We discussed it bunch of times and consensus is that we should return non-2xx code for case where data is null and 2xx for cases where data contains some data
https://github.com/graphql/graphql-over-http/blob/master/spec/GraphQLOverHTTP.md#status-codes

@proehlen
Copy link

proehlen commented Jan 25, 2021

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

@MatthiasKunnen
Copy link
Contributor

I agree with @proehlen, I also wished to use handleRuntimeQueryErrorFn to add error info in the response headers. It might be true that you could use the function in a way that violates the spec but its existence and multiple usages for it, don't.

@acao
Copy link
Member

acao commented Jan 25, 2021

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?

@derek-miller
Copy link
Author

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.

@acao acao reopened this Feb 2, 2021
@MatthiasKunnen
Copy link
Contributor

I think it should be revisited and implemented in such a way that it cannot violate the spec if desired.

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.

@acao
Copy link
Member

acao commented Feb 2, 2021

IMO, the most important thing is that the default setting does not violate the spec.

💯. 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 GraphiQL in a number of cases.

to limit support requests when people diverge from spec, we can add a “use at your own risk” warning perhaps?

@MatthiasKunnen
Copy link
Contributor

MatthiasKunnen commented Feb 2, 2021

We could make the description as follows:

An optional function which can be used to change the status code or headers of the response in case of a runtime query error. By default, the status code is set to 500.
To ensure compatibility, make sure to take note of the GraphQL spec regarding status codes.

@mantou132
Copy link

mantou132 commented Jul 3, 2021

You can return 400 via customExecutefn:

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.

@justinmchase
Copy link

@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 statusCode field on it, you should just use that. This will make it an extremely easy-to-use api 99% of the time. You just make a custom object, throw it. Boom done.

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.

@MatthiasKunnen MatthiasKunnen linked a pull request Sep 15, 2021 that will close this issue
@viveleroi
Copy link

I've noticed that when I return an error for a query the status is 500 but when there's an error in a mutation, the status is 200.

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.

@joshribakoff-sm
Copy link

joshribakoff-sm commented Jul 22, 2022

I believe the authors of the library are misreading the spec:

If the GraphQL response contains the {data} entry and it is {null}, then the server SHOULD reply with a 2xx status code and it is RECOMMENDED it replies with 200 status code.

My response contains a null data entry as described, and yet it still corrupts the status code.

@adam-nielsen
Copy link

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:

The server SHOULD NOT use a 4xx or 5xx status code.

since the client cannot determine if an application/json response with arbitrary status code is a well-formed GraphQL response (because it cannot trust the source) the server must use 200 status code to guarantee to the client that the response has not been generated or modified by an intermediary.

If the GraphQL response contains the {data} entry and it is {null}, then the server SHOULD reply with a 2xx status code and it is RECOMMENDED it replies with 200 status code.

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:

import { execute } from 'graphql';

app.use('/graphql', graphqlHTTP({
  schema: schema,
  async customExecuteFn(args) {
    const result = await execute(args);
    // Force express-graphql to return HTTP 200 on errors.  Without this it
    // returns HTTP 500, so clients don't show the actual error.
    if (result.errors?.[0]) result.data = {};
    return result;
  },
}));

@enisdenjo
Copy link
Member

This library has been deprecated and this repo will be archived soon. It has been superseded by graphql-http.

Furthermore, if you seek a fully-featured, well-maintained and performant server - I heavily recommend GraphQL Yoga!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet