-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Error extensions improvements #6759
Error extensions improvements #6759
Conversation
🦋 Changeset detectedLatest commit: 7618722 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
✅ Deploy Preview for apollo-server-docs ready!
To edit notification comments on pull requests, go to your Netlify site settings. |
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 7618722:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, declaration merging is problematic so if you think it will cause an issue removing it is the right call.
One possible alternative would be to do a breaking change and just move stacktrace
at the top level of extensions
.
@IvanGoncharov out of curiosity, why does that help? (It does seem reasonable though, esp since we already moved code out.) |
BTW, we need to add the change of |
2e87966
to
aa31257
Compare
declare module
for error extensions
@IvanGoncharov OK, I've made a few more changes along these lines. |
expect(error.extensions.code).toEqual(code); | ||
// stacktrace should exist under exception | ||
expect(error.extensions.exception.stacktrace).toBeDefined(); | ||
expect(error.extensions?.key).toEqual(key); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extensions
are always present, you shouldn't need optional chaining here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@IvanGoncharov It's actually optional on GraphQLFormattedError, which is what this is.
@@ -66,17 +67,18 @@ export function normalizeAndFormatErrors( | |||
|
|||
if (originalErrorEnumerableEntries.length > 0) { | |||
extensions.exception = { | |||
...extensions.exception, | |||
...ensureObject(extensions.exception), | |||
...Object.fromEntries(originalErrorEnumerableEntries), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's increasing the scope of this PR, but maybe we can remove the behavior of passing all enumerable properties as extensions.exception
.
This practice can be considered as information leaking, similar to "did you mean".
Now that we gave formatError
access to the original error, users can peek and choose what properties they want to put into extensions
.
So basically, I propose to just get rid of extensions.exception
.
My suggestion was based on the idea of completely removing |
833b461
to
407ef56
Compare
@IvanGoncharov I liked your suggestion and took it. I updated the PR description, which is worth re-reading. One thing I found is that documenting that the second argument to So instead I added a special case to the invocation of The one downside I see here is that if you wanted access to Another option is to change
If
Maybe this is more complicated than it's worth and we should just explain to users that they need to sometimes (but not always!) look at |
docs/source/migration.mdx
Outdated
error: unknown, | ||
) { | ||
const exception: Record<string, unknown> = { | ||
...(typeof error === 'object' ? error : null), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also to work with originalError
it can be
...(error instanceof Error ? error.originalError ?? error : null),
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm not super-worried about things like strings and arrays here... if you pass something ultra weird, then your code ends up weird. I just want to make sure TS builds.
// there is no path, this error didn't come from a resolver.) | ||
error instanceof GraphQLError && error.path && error.originalError | ||
? error.originalError | ||
: error, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
passing originalError makes sense even if path
is absent.
example errors thrown from coerceValue/serialize since they also wrapped.
So I recommend dropping the path
check here.
@glasser I think the cleanest semantic would be that |
407ef56
to
26bd3f1
Compare
@IvanGoncharov going with your suggestion, plus a new |
1f4193d
to
c4238de
Compare
@@ -843,7 +843,7 @@ export function defineIntegrationTestSuiteApolloServerTests( | |||
}, | |||
}, | |||
introspection: true, | |||
includeStackTracesInErrorResponses: true, | |||
includeStacktraceInErrorResponses: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does having this set to true have any effect on this test? Seems like the object matcher below should be looking for a stack trace, else maybe we just unset this?
Or am I misunderstanding something here maybe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm going to delete this test. Reviewing the initial PR that introduced it (#1288) I don't understand what was interesting about this random yup
package and suspect it is related to this flattening code which we have deleted. The PR description refers to schema-stitching stuff too. Since the error code is so much simpler now I doubt we have to test one random style of error.
* (like parse errors) are not unwrapped by this function. | ||
*/ | ||
export function unwrapResolverError(error: unknown): unknown { | ||
if (error instanceof GraphQLError && error.path && error.originalError) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I understand why we also need the error.path
check here.
Unrelated: I didn't find anything interesting on this path but maybe you will. Pretty sure the incoming unknown
makes any attempt at refining the return type pointless. Might only be useful in a context where you have some type info about the incoming error
argument.
export function unwrapResolverError<T = unknown>(error: T): T | Error {
if (error instanceof GraphQLError && error.path && error.originalError) {
return error.originalError;
}
return error;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@trevor-scheer Well, that's what makes this "unwrapResolverError" and not just "unwrapError".
Look at, say, SyntaxError — it's a GraphQLError defined in our code that wraps a GraphQLError received from graphql-js, adding code
as an extension. I don't think it's helpful for a formatError to strip off our SyntaxError, is it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, I was only seeing this as a type-level refinement and didn't see why we needed it to access originalError
.
This PR does a few things: (a) Moves `error.extensions.exception.stacktrace` to `error.extensions.stacktrace`. (b) Removes the rest of `error.extensions.exception`. This contained enumerable properties of the original error, which could lead to surprising information leakage. (c) Documents that change and provides a `formatError` hook in the migration guide to maintain AS3 behavior. (d) Provide an `unwrapResolverError` function in `@apollo/server/errors` to help get back the original error in the resolver error case, which is a helpful part of the documented `formatError` recommendations (since the old error formatting put stuff from that unwrapped error on the `exception` extension). (e) Gets rid of the `declare module` which made `error.extensions.exception.code`/`error.extensions.exception.stacktrace` have a non-unknown value. Note that this declaration (added in 3.5.0) was actually inaccurate as `code` really goes directly on `extensions` rather than on `exception`. We could have instead preserved the declaration and adapted it to the new location of `stacktrace` and the correct location of `code`. - Pro: `declare module` is a bit scary and doesn't always merge well if you have more than one of them (eg, if you end up with both AS3 and AS4 in your TypeScript build: AS3 had a different `declare module` where `code` was nested under `exception`). - Con: End users who catch our errors can't get "correct" typing for `error.extensions.code` or `error.extensions.stacktrace`. - Pro: That's only "correct" if you assume all GraphQLErrors use extensions compatible with the definition; it might be better to export a helper function that takes a `GraphQLError` and returns the code/exception if it looks like it has the right shape. And nobody seems to have even noticed that the declaration has been wrong for almost a year, so how much value is it providing? (f) Renames the (new in v4) constructor option includeStackTracesInErrorResponses to includeStacktraceInErrorResponses, to match the extension name. This is to some degree part of #6719 because we're concerned about the effect of the `declare module` on the Gateway transition.
c4238de
to
7618722
Compare
Until now, the refactored AS4 did not support Apollo Gateway (or any implementation of the AS3 `gateway` option). That's because `GraphQLRequestContext` is part of the API between Apollo Gateway and Apollo Server, and that type has changed in some minor but incompatible ways in AS4. (Additionally, clashes between `declare module` declarations in AS3 and AS4 caused issue, but we removed those declarations from this branch in PRs #6764 and #6759.) This commit restores gateway support. It does this by having AS4 produce an AS3-style request context object. It uses a new `@apollo/server-gateway-interface` package to define the appropriate types for connecting to Gateway. (Note: this package will be code reviewed in this PR in this repo, but once it's approved, it will move to the apollo-utils repo and be released as non-alpha there. Once we've released AS4.0.0 we can move that package back here, but trying to do some prereleases and some non-prereleases on the same branch seems challenging.) This PR removes the top-level `executor` function, which is redundant with the `gateway` option. (Internally, the relevant field is now named `gatewayExecutor`.) Some types had been parametrized by `TContext`, because in AS3, `GraphQLExecutor` (now `GatewayExecutor`) appeared to take a `<TContext>`. However, even though the type itself took a generic argument, its main use in the return from `gateway.load` implicitly hardcoded the default `TContext`. So we are doubling down on that and only allowing `GraphQLExecutor` to use AS3's default `TContext`, the quite flexible `Record<string, any>`. Most of the way toward #6719.
Until now, the refactored AS4 did not support Apollo Gateway (or any implementation of the AS3 `gateway` option). That's because `GraphQLRequestContext` is part of the API between Apollo Gateway and Apollo Server, and that type has changed in some minor but incompatible ways in AS4. (Additionally, clashes between `declare module` declarations in AS3 and AS4 caused issue, but we removed those declarations from this branch in PRs #6764 and #6759.) This commit restores gateway support. It does this by having AS4 produce an AS3-style request context object. It uses a new `@apollo/server-gateway-interface` package to define the appropriate types for connecting to Gateway. (Note: this package will be code reviewed in this PR in this repo, but once it's approved, it will move to the apollo-utils repo and be released as non-alpha there. Once we've released AS4.0.0 we can move that package back here, but trying to do some prereleases and some non-prereleases on the same branch seems challenging.) This PR removes the top-level `executor` function, which is redundant with the `gateway` option. (Internally, the relevant field is now named `gatewayExecutor`.) Some types had been parametrized by `TContext`, because in AS3, `GraphQLExecutor` (now `GatewayExecutor`) appeared to take a `<TContext>`. However, even though the type itself took a generic argument, its main use in the return from `gateway.load` implicitly hardcoded the default `TContext`. So we are doubling down on that and only allowing `GraphQLExecutor` to use AS3's default `TContext`, the quite flexible `Record<string, any>`. Most of the way toward #6719. Co-authored-by: Trevor Scheer <[email protected]>
This PR does a few things:
(a) Moves
error.extensions.exception.stacktrace
toerror.extensions.stacktrace
.(b) Removes the rest of
error.extensions.exception
. This containedenumerable properties of the original error, which could lead to
surprising information leakage.
(c) Documents that change and provides a
formatError
hookin the migration guide to maintain AS3 behavior.
(d) Provide an
unwrapResolverError
function in@apollo/server/errors
to help get back the original error in the resolver error case, which is
a helpful part of the documented
formatError
recommendations(since the old error formatting put stuff from that unwrapped error
on the
exception
extension).(e) Gets rid of the
declare module
which madeerror.extensions.exception.code
/error.extensions.exception.stacktrace
have a non-unknown value. Note that this declaration (added in 3.5.0) was
actually inaccurate as
code
really goes directly onextensions
rather thanon
exception
. We could have instead preserved the declarationand adapted it to the new location of
stacktrace
and the correctlocation of
code
.Pro:
declare module
is a bit scary and doesn't always merge well ifyou have more than one of them (eg, if you end up with both AS3 and AS4
in your TypeScript build: AS3 had a different
declare module
wherecode
was nested underexception
).Con: End users who catch our errors can't get "correct" typing for
error.extensions.code
orerror.extensions.stacktrace
.Pro: That's only "correct" if you assume all GraphQLErrors use
extensions compatible with the definition; it might be better to export
a helper function that takes a
GraphQLError
and returns thecode/exception if it looks like it has the right shape. And nobody seems
to have even noticed that the declaration has been wrong for almost a
year, so how much value is it providing?
(f) Renames the (new in v4) constructor option
includeStackTracesInErrorResponses to
includeStacktraceInErrorResponses, to match the extension name.
(g) Removes a test around error handling a particular
yup
style of errorsthat is probably not relevant any more.
This is to some degree part of #6719 because we're concerned about the
effect of the
declare module
on the Gateway transition.