Repository navigation
feat: typed AppSync resolver errors (#385) - #386
Merged
Merged
Conversation
Add errors.appsync namespace (AppSyncError base plus ForbiddenError,
NotFoundError, ValidationError, ConflictError). createAppSyncHandler now
returns the direct-Lambda error envelope { data, errorType, errorMessage,
errorInfo } and logs at warn level when a resolver throws an AppSyncError,
so AppSync maps it to a GraphQL error without failing the invocation.
Untyped errors still log at error level and rethrow.
Closes #385
…ndler
addLambdaResolver now creates an APPSYNC_JS pipeline resolver whose
response handler converts the handler error envelope into a GraphQL
error via util.error/util.appendError. A direct-Lambda resolver with no
response handler would surface the { data, errorType, errorMessage,
errorInfo } envelope as field data instead of raising a GraphQL error,
so the typed errors.appsync.* errors never reached the client. The
errorInfo field is wired through util.error's fourth argument.
Document the required resolver wiring and the HTTP-vs-appsync
ForbiddenError/NotFoundError name distinction in llm.md.
The GraphQlApi.addLambdaResolver response handler decided an invocation was an error from a truthy result.errorMessage, so a resolver whose legitimate return value had an errorMessage field raised a spurious GraphQL error. createAppSyncHandler now stamps a reserved __appSyncError sentinel on the error envelope, and the resolver response handler branches on that sentinel instead of on errorMessage. Tests pin the sentinel at both ends and the success passthrough. llm.md documents the resolver topology change and the cdk diff recommendation for upgrades.
createAppSyncHandler returned the sentinel envelope unconditionally on an errors.appsync.* throw. On the default UNIT resolver path (no response handler) AppSync surfaced the raw envelope as field data and the typed error was lost, and the Lambda Errors metric never incremented. Gate the envelope-return on process.env.APPSYNC_TYPED_ERRORS === 'true'. GraphQlApi.addLambdaResolver injects APPSYNC_TYPED_ERRORS=true only on the typedErrors:true pipeline path. When the flag is absent, the handler rethrows the typed error, restoring loud failure on the default path. Untyped errors always rethrow.
…ppSyncHandler Map errors.HttpError statusCode to a GraphQL errorType (400 BadRequest, 401 Unauthorized, 403 Forbidden, 404 NotFound, other Error) and route it through the same APPSYNC_TYPED_ERRORS gate and sentinel envelope as AppSyncError. HTTP errors thrown by the shared authorizer assertions no longer fall through to the untyped rethrow when typed errors are active.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #385.
What this does
Gives
createAppSyncHandlertyped-error support so expected resolver failures (forbidden, not-found, validation, conflict) surface as GraphQL errors without failing the Lambda invocation, keeping them off the LambdaErrorsmetric. The resolver-side support is opt-in so upgrading consumers see no change unless they ask for it.Changes
src/lambda/appsync-errors.ts(new):AppSyncErrorbase carryingreadonly errorType: stringand optionalerrorInfo, with subclassesForbiddenError(Forbidden),NotFoundError(NotFound),ValidationError(ValidationError),ConflictError(Conflict). Re-exported fromerrors.tsas a nested namespace, so consumers useerrors.appsync.*and the existing HTTPerrors.ForbiddenError/errors.NotFoundErrorare unchanged.src/lambda/handler.ts: on catching anAppSyncError,createAppSyncHandlerlogs atwarnand returns a sentinel-stamped envelope ({ __appSyncError: true, data: null, errorType, errorMessage, errorInfo }) instead of rethrowing. Untyped errors still log aterrorand rethrow.src/constructs/graphql.ts: new API-widetypedErrors?: booleanprop onGraphQlApiProps(@default false). When unset,addLambdaResolversynthesizes the pre-createAppSyncHandler: support typed errors so expected failures return GraphQL errors without failing the Lambda invocation #385 plain UNIT direct-Lambda resolver, so upgrading consumers get zero resolver-topology diff. Whentrue, it synthesizes an APPSYNC_JS pipeline resolver whose response handler converts the envelope into a GraphQL error viautil.error(message, errorType, null, errorInfo).test/lambda/appsync-handler.test.ts(handler behavior) andtest/constructs/graphql.test.ts(both the default UNIT path and the opt-in PIPELINE path).llm.md: documents the opt-in, that the default is unchanged UNIT resolvers (nocdk diffsurprise), and the handler/pipeline matched-pair interaction.docs/experiments/appsync-unit-resolver-error-probe.md: a standalone probe spec used to settle, against a live AppSync API, that a bare UNIT direct-Lambda resolver cannot surface the typed-error envelope on a single Invoke. The probe confirmed the pipeline is required, which is why the opt-in path uses APPSYNC_JS.Why opt-in
A live AppSync probe confirmed that a UNIT direct-Lambda resolver returns the
{ errorType, errorMessage }object as field data on a single Invoke rather than mapping it to a GraphQL error. Typed errors therefore require the APPSYNC_JS pipeline, which is a resolver-topology change. Gating it behindtypedErrorsmeans consumers who upgrade without opting in keep plain UNIT resolvers and see nocdk diff. EnablingtypedErrorsupdates resolvers in place (logical IDs unchanged) and adds oneFunctionConfigurationper Lambda resolver; runcdk diffafter enabling.Compatibility
Additive and backward compatible. Default behavior matches pre-#385 byte-for-byte (verified against commit
e868b6a). ThetypedErrorsflag applies to all Lambda resolvers on an API.Verification
pnpm run build,pnpm run test(128 pass),pnpm run eslintclean,npx projenzero drift.Handler/resolver coupling (enforced, not just documented)
createAppSyncHandlerreturns the sentinel envelope only whenprocess.env.APPSYNC_TYPED_ERRORS === 'true', otherwise it rethrows the typed error (invocation fails loudly, same as pre-#385).addLambdaResolverinjectsAPPSYNC_TYPED_ERRORS=trueinto the resolver Lambda only on thetypedErrors: truepipeline path, the same path that attaches the response handler that can translate the envelope. This closes a regression where a handler throwingerrors.appsync.*on the default UNIT path would return the raw envelope as field data with the error type silently dropped. Default when the env var is absent is rethrow, so hand-wired handlers used withoutGraphQlApistay safe and must setAPPSYNC_TYPED_ERRORS=truethemselves to get envelope behavior.Auth / HTTP errors on the AppSync path
createAppSyncHandleralso translates HTTP errors (errors.HttpErrorand subclasses) into typed GraphQL errors, routed through the sameAPPSYNC_TYPED_ERRORSgate. This covers the errors the shared authorizer insrc/lambda/auth.tsthrows fromassertAuthenticated/assertGroup/assertAdmin, which are expected permission failures on the AppSync path but were previously rethrown as untyped faults. Status-to-errorType mapping: 400 BadRequest, 401 Unauthorized, 403 Forbidden, 404 NotFound, any other Error.auth.ts, the HTTPerrors.*classes, andcreateHttpHandlerare unchanged; the translation happens only at the AppSync boundary. Same rethrow-when-not-activated rule applies, so a default UNIT resolver whose handler hits an auth assertion still fails loudly unlesstypedErrorsis active.