-
Notifications
You must be signed in to change notification settings - Fork 166
feat(event-handler): add support for error handling in AppSync GraphQL #4317
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
base: main
Are you sure you want to change the base?
Changes from all commits
9f97957
b588562
c36c93c
150157e
432f5d6
081e805
b7a2846
d1a499d
2e1b853
3dc04b6
5df6159
32919da
d024aad
66ff42e
aac7f9f
ad1793e
737248d
e93530c
737a766
b4d307c
22433a6
eb049cf
47c6c6a
3a7d8dc
69ed880
e51c43b
e018758
fb08824
433f10a
4952c79
85b5c8c
5aaac53
c79f5bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -148,6 +148,30 @@ You can access the original Lambda event or context for additional information. | |
|
||
1. The `event` parameter contains the original AppSync event and has type `AppSyncResolverEvent` from the `@types/aws-lambda`. | ||
|
||
### Exception Handling | ||
|
||
You can use the **`exceptionHandler`** method with any exception. This allows you to handle common errors outside your resolver and return a custom response. | ||
|
||
You can use a VTL response mapping template to detect these custom responses and forward them to the client gracefully. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The recommended way to do response mapping is JS now, not VTL, so we should include an example of how to do it that way. |
||
|
||
=== "Exception Handling" | ||
|
||
```typescript hl_lines="11-18 21-23" | ||
--8<-- "examples/snippets/event-handler/appsync-graphql/exceptionHandling.ts" | ||
``` | ||
|
||
=== "VTL Response Mapping Template" | ||
|
||
```velocity hl_lines="1-3" | ||
--8<-- "examples/snippets/event-handler/appsync-graphql/exceptionHandlingResponseMapping.vtl" | ||
``` | ||
|
||
=== "Exception Handling response" | ||
|
||
```json hl_lines="11 20" | ||
--8<-- "examples/snippets/event-handler/appsync-graphql/exceptionHandlingResponse.json" | ||
``` | ||
|
||
### Logging | ||
|
||
By default, the utility uses the global `console` logger and emits only warnings and errors. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; | ||
import { Logger } from '@aws-lambda-powertools/logger'; | ||
import { AssertionError } from 'assert'; | ||
import type { Context } from 'aws-lambda'; | ||
|
||
const logger = new Logger({ | ||
serviceName: 'MyService', | ||
}); | ||
const app = new AppSyncGraphQLResolver({ logger }); | ||
|
||
app.exceptionHandler(AssertionError, async (error) => { | ||
return { | ||
error: { | ||
message: error.message, | ||
type: error.name, | ||
}, | ||
}; | ||
}); | ||
|
||
app.onQuery('createSomething', async () => { | ||
throw new AssertionError({ | ||
message: 'This is an assertion Error', | ||
}); | ||
}); | ||
|
||
export const handler = async (event: unknown, context: Context) => | ||
app.resolve(event, context); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
{ | ||
"data": { | ||
"createSomething": null | ||
}, | ||
"errors": [ | ||
{ | ||
"path": [ | ||
"createSomething" | ||
], | ||
"data": null, | ||
"errorType": "AssertionError", | ||
"errorInfo": null, | ||
"locations": [ | ||
{ | ||
"line": 2, | ||
"column": 3, | ||
"sourceName": null | ||
} | ||
], | ||
"message": "This is an assertion Error" | ||
} | ||
] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
#if (!$util.isNull($ctx.result.error)) | ||
$util.error($ctx.result.error.message, $ctx.result.error.type) | ||
#end | ||
|
||
$utils.toJson($ctx.result) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import type { GenericLogger } from '@aws-lambda-powertools/commons/types'; | ||
import type { | ||
ExceptionHandler, | ||
ExceptionHandlerOptions, | ||
ExceptionHandlerRegistryOptions, | ||
} from '../types/appsync-graphql.js'; | ||
|
||
/** | ||
* Registry for storing exception handlers for GraphQL resolvers in AWS AppSync GraphQL API's. | ||
*/ | ||
class ExceptionHandlerRegistry { | ||
/** | ||
* A map of registered exception handlers, keyed by their error class name. | ||
*/ | ||
protected readonly handlers: Map<string, ExceptionHandlerOptions> = new Map(); | ||
/** | ||
* A logger instance to be used for logging debug and warning messages. | ||
*/ | ||
readonly #logger: Pick<GenericLogger, 'debug' | 'warn' | 'error'>; | ||
|
||
public constructor(options: ExceptionHandlerRegistryOptions) { | ||
this.#logger = options.logger; | ||
} | ||
|
||
/** | ||
* Registers an exception handler for a specific error class. | ||
* | ||
* If a handler for the given error class is already registered, it will be replaced and a warning will be logged. | ||
* | ||
* @param options - The options containing the error class and its associated handler. | ||
*/ | ||
public register(options: ExceptionHandlerOptions<Error>): void { | ||
const { error, handler } = options; | ||
const errorName = error.name; | ||
|
||
this.#logger.debug(`Adding exception handler for error class ${errorName}`); | ||
|
||
if (this.handlers.has(errorName)) { | ||
this.#logger.warn( | ||
`An exception handler for error class '${errorName}' is already registered. The previous handler will be replaced.` | ||
); | ||
} | ||
|
||
this.handlers.set(errorName, { | ||
error, | ||
handler: handler as ExceptionHandler, | ||
}); | ||
} | ||
|
||
/** | ||
* Resolves and returns the appropriate exception handler for a given error instance. | ||
* | ||
* This method attempts to find a registered exception handler based on the error class name. | ||
* If a matching handler is found, it is returned; otherwise, `undefined` is returned. | ||
* | ||
* @param error - The error instance for which to resolve an exception handler. | ||
*/ | ||
public resolve(error: Error): ExceptionHandler | undefined { | ||
const errorName = error.name; | ||
this.#logger.debug(`Looking for exception handler for error: ${errorName}`); | ||
|
||
const handlerOptions = this.handlers.get(errorName); | ||
if (handlerOptions) { | ||
this.#logger.debug(`Found exact match for error class: ${errorName}`); | ||
return handlerOptions.handler; | ||
} | ||
|
||
this.#logger.debug(`No exception handler found for error: ${errorName}`); | ||
return undefined; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We return |
||
} | ||
|
||
/** | ||
* Checks if there are any registered exception handlers. | ||
*/ | ||
public hasHandlers(): boolean { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need this function? I think it's sufficient for the client to call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that can also be done too |
||
return this.handlers.size > 0; | ||
} | ||
} | ||
|
||
export { ExceptionHandlerRegistry }; |
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.
Is this intentional?
Uh oh!
There was an error while loading. Please reload this page.
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, as other version is failing in my local
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 remember you modified this in a previous PR and had to revert it back - I am unsure what's happening here, but I'd say let's not include it as part of this PR.
If you need this to get the docs running locally for now keep the change in your local branch if possible.