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

Rethink ApolloError #6053

Closed
glasser opened this issue Jan 26, 2022 · 7 comments
Closed

Rethink ApolloError #6053

glasser opened this issue Jan 26, 2022 · 7 comments
Assignees
Milestone

Comments

@glasser
Copy link
Member

glasser commented Jan 26, 2022

ApolloError in apollo-server-errors adds some functionality to GraphQLError from graphql-js. graphql-js may have already caught up with this to some degree, and other functionality perhaps is general-purpose enough that it could easily just move to graphql-js. Let's figure out exactly what the value of having our own subclass is and see what we can do to reduce complexity.

@benjamn
Copy link
Member

benjamn commented Jan 31, 2022

Another implementation of ApolloError is exported by @apollo/client/errors here. I wonder if we can consolidate these implementations?

glasser added a commit that referenced this issue Feb 5, 2022
Not sure if we'll stick with this: depends on what
we end up doing in #6053. Maybe it will still be helpful
for libraries to be able to use AS errors without depending
on a specific pinned version of AS. But for now this is simpler.
@glasser
Copy link
Member Author

glasser commented Feb 5, 2022

On version-4 I moved the code from apollo-server-errors into the main package.

That said, it's possible we might decide that errors do deserve their own package, so that a library could implement an ApolloError (if it still exists) or throw a specific error like BadInputError without needing a dependency on @apollo/server that could pin a too-specific version. (eg, @apollo/gateway depends on apollo-server-errors and we'd really like it to not depend on @apollo/server.)

@glasser
Copy link
Member Author

glasser commented Apr 13, 2022

Worth looking at #6316 to see if some of its ideas are relevant.

@glasser
Copy link
Member Author

glasser commented Jun 2, 2022

Major progress on version-4 in #6355. We still need to describe those changes and maybe make a few more like renaming formatApolloErrors.

glasser added a commit that referenced this issue Jun 2, 2022
Previously, errors that weren't specially cased as part of the "request
pipeline" were not observable by plugins. (In some but not all cases,
they were visible to formatError hooks.) Some "HTTP-level" errors were
handled by returning text/plain responses, whereas many other errors
were handled by returning GraphQL-style JSON responses (using
the formatError hook).

This change makes things more consistent by returning all errors as JSON
responses (using the formatError hook). In addition, for better
observability, several new top-level plugin APIs are introduced that are
called when various kinds of errors occur. Specifically:

- New plugin hook `startupDidFail`. This is called once if startup fails
  (eg, if a serverWillStart plugin throws or if gateway fails to load
  the schema). (If this hook throws, its error is logged.) Future calls to
  executeHTTPGraphQLRequest (if the server was started in the
  background) now return a formatted JSON error instead of throwing.

- New plugin hook `contextCreationDidFail`. (If this hook throws, its
  error is logged.) As before, these errors are sent as formatted JSON
  errors.

- New plugin hook `invalidRequestWasReceived`. This is called for all
  the errors that occur before being able to construct a GraphQLRequest
  from an HTTPGraphQLRequest; this includes things like the top-level
  fields being of the wrong type, missing body, CSRF failure, etc).
  These errors are now sent as formatted JSON instead of as text/plain.

- New plugin hook `unexpectedErrorProcessingRequest`. This is called if
  `processGraphQLRequest` throws (typically because a plugin hook
  threw). We are making the assumption that this means there is a
  programming error, so we are making the behavior change that the
  thrown error will be logged and "Internal server error" will be sent
  in the HTTP response.

- Fix grammar of "GET supports only query operation" and don't return it
  if the operation is unknown.

Fixes #6140. Part of #6053.
glasser added a commit that referenced this issue Jun 3, 2022
Previously, errors that weren't specially cased as part of the "request
pipeline" were not observable by plugins. (In some but not all cases,
they were visible to formatError hooks.) Some "HTTP-level" errors were
handled by returning text/plain responses, whereas many other errors
were handled by returning GraphQL-style JSON responses (using
the formatError hook).

This change makes things more consistent by returning all errors as JSON
responses (using the formatError hook). In addition, for better
observability, several new top-level plugin APIs are introduced that are
called when various kinds of errors occur. Specifically:

- New plugin hook `startupDidFail`. This is called once if startup fails
  (eg, if a serverWillStart plugin throws or if gateway fails to load
  the schema). (If this hook throws, its error is logged.) Future calls to
  executeHTTPGraphQLRequest (if the server was started in the
  background) now return a formatted JSON error instead of throwing.

- New plugin hook `contextCreationDidFail`. (If this hook throws, its
  error is logged.) As before, these errors are sent as formatted JSON
  errors.

- New plugin hook `invalidRequestWasReceived`. This is called for all
  the errors that occur before being able to construct a GraphQLRequest
  from an HTTPGraphQLRequest; this includes things like the top-level
  fields being of the wrong type, missing body, CSRF failure, etc).
  These errors are now sent as formatted JSON instead of as text/plain.

- New plugin hook `unexpectedErrorProcessingRequest`. This is called if
  `processGraphQLRequest` throws (typically because a plugin hook
  threw). We are making the assumption that this means there is a
  programming error, so we are making the behavior change that the
  thrown error will be logged and "Internal server error" will be sent
  in the HTTP response.

- Fix grammar of "GET supports only query operation" and don't return it
  if the operation is unknown.

Fixes #6140. Part of #6053.
@glasser
Copy link
Member Author

glasser commented Jul 13, 2022

In addition to writing the docs, @trevor-scheer and I have some questions we're thinking about with regards to the exported XError types. It's now quite easy to define your own subclasses with particular error codes. So the questions are:

  • Do we really even need AuthenticationError and ForbiddenError, which are not used internally and are just provided for users?
  • Do we actually need to export the other ones like SyntaxError, or should we just use it internally and document that people who want to see if an error is of a particular type should check the code extension instead?
  • If we do want to export all of them then we should be consistent and export PersistedQueryNotSupportedError, OperationResolutionError, and BadRequestError.
  • Would it be cool to have a little function like const AuthenticationError = makeErrorClass('AuthenticationError', 'UNAUTHENTICATED')? Or maybe not.

@IvanGoncharov
Copy link
Member

  • Do we really even need AuthenticationError and ForbiddenError, which are not used internally and are just provided for users?

@glasser I agree we should remove them.

  • Do we actually need to export the other ones like SyntaxError, or should we just use it internally and document that people who want to see if an error is of a particular type should check the code extension instead?

No, I think exposing enum with codes would be enough.
So instead of error instanceof SyntaxError you can do error.extensions?.code === ApolloServerErrorCode.GRAPHQL_PARSE_FAILED.

  • If we do want to export all of them then we should be consistent and export PersistedQueryNotSupportedError, OperationResolutionError, and BadRequestError.

No, I think they don't add a lot of value.

  • Would it be cool to have a little function like const AuthenticationError = makeErrorClass('AuthenticationError', 'UNAUTHENTICATED')? Or maybe not.

This function feels more like ES5 (before modern ES6 class syntax).
It saves a few repetitive line:

export class PersistedQueryNotSupportedError extends GraphQLErrorWithCode {
  constructor() {
    super(
      'PersistedQueryNotSupported',
      ApolloServerErrorCode.PERSISTED_QUERY_NOT_SUPPORTED,
    );
    this.name = 'PersistedQueryNotSupportedError';
  }
}

But I don't think that's a big saving compared to messing with class prototypes dynamically.

I implement above suggestions in #6705

@glasser
Copy link
Member Author

glasser commented Jul 20, 2022

With #6705 merged and #6696 incorporated into #6687 I think we can declare this done!

@glasser glasser closed this as completed Jul 20, 2022
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Apr 20, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants