Skip to content

Commit a8add74

Browse files
AppSync Events
1 parent 1c37a12 commit a8add74

15 files changed

+230
-108
lines changed

docs/core/event_handler/appsync_events.md

+124-6
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ You must have an existing AppSync Events API with real-time capabilities enabled
6565

6666
### AppSync request and response format
6767

68-
AppSync Events uses a specific event format for Lambda requests and responses. In most scenarios, Powertools simplifies this interaction by automatically formatting resolver returns to match the expected AppSync response structure.
68+
AppSync Events uses a specific event format for Lambda requests and responses. In most scenarios, Powertools for AWS simplifies this interaction by automatically formatting resolver returns to match the expected AppSync response structure.
6969

7070
=== "payload_request.json"
7171

@@ -95,7 +95,7 @@ AppSync Events uses a specific event format for Lambda requests and responses. I
9595

9696
When processing events with Lambda, you can return errors to AppSync in three ways:
9797

98-
* **Error per item:** Return an `error` key within each individual item's response. AppSync Events expects this format for item-specific errors.
98+
* **Item specific error:** Return an `error` key within each individual item's response. AppSync Events expects this format for item-specific errors.
9999
* **Fail entire request:** Return a JSON object with a top-level `error` key. This signals a general failure, and AppSync treats the entire request as unsuccessful.
100100
* **Unauthorized exception**: Raise the **UnauthorizedException** exception to reject a subscribe or publish request with HTTP 403.
101101

@@ -124,7 +124,7 @@ You can define your handlers for different event types using the `app.on_publish
124124

125125
You can use wildcard patterns to create catch-all handlers for multiple channels or namespaces. This is particularly useful for centralizing logic that applies to multiple channels.
126126

127-
When multiple handlers could match the same event, the most specific pattern takes precedence.
127+
When an event matches with multiple handlers, the most specific pattern takes precedence.
128128

129129
=== "working_with_wildcard_resolvers.py"
130130

@@ -197,11 +197,28 @@ When processing batch of items with `aggregate=True`, you must format the payloa
197197
--8<-- "examples/event_handler_appsync_events/src/working_with_error_handling_response.json"
198198
```
199199

200-
#### Rejecting the entire request
200+
If instead you want to fail the entire batch, you can throw an exception. This will cause the Event Handler to return an error response to AppSync and fail the entire batch.
201201

202-
??? warning "Raising `UnauthorizedException` will cause the Lambda invocation to fail."
202+
=== "working_with_error_handling_multiple.py"
203+
204+
```python hl_lines="5 6 13"
205+
--8<-- "examples/event_handler_appsync_events/src/working_with_error_handling_multiple.py"
206+
```
207+
208+
=== "working_with_error_handling_response.json"
209+
210+
```python hl_lines="5 6 13"
211+
--8<-- "examples/event_handler_appsync_events/src/working_with_error_handling_response.json"
212+
```
213+
214+
#### Authorization control
203215

204-
You can also reject the entire payload by raising an `UnauthorizedException`. This prevents Powertools from processing any messages and causes the Lambda invocation to fail, returning an error to AppSync.
216+
!!! warning "Raising `UnauthorizedException` will cause the Lambda invocation to fail."
217+
218+
You can also do content based authorization for channel by raising the `UnauthorizedException` exception. This can cause two situations:
219+
220+
- **When working with publish events** Powertools for AWS stop processing messages and subscribers will not receive any message.
221+
- **When working with subscribe events** the subscription won't be established.
205222

206223
=== "working_with_error_handling.py"
207224

@@ -240,6 +257,107 @@ You can access to the original Lambda event or context for additional informatio
240257
--8<-- "examples/event_handler_appsync_events/src/accessing_event_and_context.py"
241258
```
242259

260+
## Event Handler workflow
261+
262+
#### Working with single items
263+
<center>
264+
```mermaid
265+
sequenceDiagram
266+
participant Client
267+
participant AppSync
268+
participant Lambda
269+
participant EventHandler
270+
note over Client,EventHandler: Individual Event Processing (aggregate=False)
271+
Client->>+AppSync: Send multiple events to channel
272+
AppSync->>+Lambda: Invoke Lambda with batch of events
273+
Lambda->>+EventHandler: Process events with aggregate=False
274+
loop For each event in batch
275+
EventHandler->>EventHandler: Process individual event
276+
end
277+
EventHandler-->>-Lambda: Return array of processed events
278+
Lambda-->>-AppSync: Return event-by-event responses
279+
AppSync-->>-Client: Report individual event statuses
280+
```
281+
</center>
282+
283+
284+
#### Working with aggregated items
285+
<center>
286+
```mermaid
287+
sequenceDiagram
288+
participant Client
289+
participant AppSync
290+
participant Lambda
291+
participant EventHandler
292+
note over Client,EventHandler: Aggregate Processing Workflow
293+
Client->>+AppSync: Send multiple events to channel
294+
AppSync->>+Lambda: Invoke Lambda with batch of events
295+
Lambda->>+EventHandler: Process events with aggregate=True
296+
EventHandler->>EventHandler: Batch of events
297+
EventHandler->>EventHandler: Process entire batch at once
298+
EventHandler->>EventHandler: Format response for each event
299+
EventHandler-->>-Lambda: Return aggregated results
300+
Lambda-->>-AppSync: Return success responses
301+
AppSync-->>-Client: Confirm all events processed
302+
```
303+
</center>
304+
305+
#### Authorization fails for publish
306+
307+
<center>
308+
```mermaid
309+
sequenceDiagram
310+
participant Client
311+
participant AppSync
312+
participant Lambda
313+
participant EventHandler
314+
note over Client,EventHandler: Publish Event Authorization Flow
315+
Client->>AppSync: Publish message to channel
316+
AppSync->>Lambda: Invoke Lambda with publish event
317+
Lambda->>EventHandler: Process publish event
318+
alt Authorization Failed
319+
EventHandler->>EventHandler: Authorization check fails
320+
EventHandler->>Lambda: Raise UnauthorizedException
321+
Lambda->>AppSync: Return error response
322+
AppSync--xClient: Message not delivered
323+
AppSync--xAppSync: No distribution to subscribers
324+
else Authorization Passed
325+
EventHandler->>Lambda: Return successful response
326+
Lambda->>AppSync: Return processed event
327+
AppSync->>Client: Acknowledge message
328+
AppSync->>AppSync: Distribute to subscribers
329+
end
330+
```
331+
</center>
332+
333+
#### Authorization fails for subscribe
334+
<center>
335+
```mermaid
336+
sequenceDiagram
337+
participant Client
338+
participant AppSync
339+
participant Lambda
340+
participant EventHandler
341+
note over Client,EventHandler: Subscribe Event Authorization Flow
342+
Client->>AppSync: Request subscription to channel
343+
AppSync->>Lambda: Invoke Lambda with subscribe event
344+
Lambda->>EventHandler: Process subscribe event
345+
alt Authorization Failed
346+
EventHandler->>EventHandler: Authorization check fails
347+
EventHandler->>Lambda: Raise UnauthorizedException
348+
Lambda->>AppSync: Return error response
349+
AppSync--xClient: Subscription denied (HTTP 403)
350+
else Authorization Passed
351+
EventHandler->>Lambda: Return successful response
352+
Lambda->>AppSync: Return authorization success
353+
AppSync->>Client: Subscription established
354+
end
355+
```
356+
</center>
357+
358+
359+
360+
243361
## Testing your code
244362

245363
You can test your event handlers by passing a mocked or actual AppSync Events Lambda event.

examples/event_handler_appsync_events/src/accessing_event_and_context.py

+3
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010

1111
app = AppSyncEventsResolver()
1212

13+
1314
class ValidationError(Exception):
1415
pass
1516

17+
1618
@app.on_publish("/default/channel1")
1719
def handle_channel1_publish(payload: dict[str, Any]):
1820
# Access the full event and context
@@ -31,5 +33,6 @@ def handle_channel1_publish(payload: dict[str, Any]):
3133
"timeRemaining": remaining_time,
3234
}
3335

36+
3437
def lambda_handler(event: dict, context: LambdaContext):
3538
return app.resolve(event, context)

examples/event_handler_appsync_events/src/getting_started_with_publish_events.py

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
app = AppSyncEventsResolver()
1111

12+
1213
@app.on_publish("/default/channel")
1314
def handle_channel1_publish(payload: dict[str, Any]):
1415
# Process the payload for this specific channel
@@ -17,5 +18,6 @@ def handle_channel1_publish(payload: dict[str, Any]):
1718
"original_payload": payload,
1819
}
1920

21+
2022
def lambda_handler(event: dict, context: LambdaContext):
2123
return app.resolve(event, context)

examples/event_handler_appsync_events/src/getting_started_with_subscribe_events.py

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
app = AppSyncEventsResolver()
1212

13+
1314
@app.on_subscribe("/*")
1415
def handle_all_subscriptions():
1516
path = app.current_event.info.channel_path
@@ -20,12 +21,14 @@ def handle_all_subscriptions():
2021

2122
return True
2223

24+
2325
def is_authorized(path: str):
2426
# Your authorization logic here
2527
if path == "not_allowed_path_here":
2628
return False
2729

2830
return True
2931

32+
3033
def lambda_handler(event: dict, context: LambdaContext):
3134
return app.resolve(event, context)

examples/event_handler_appsync_events/src/getting_started_with_testing_publish.py

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def __init__(self):
1414
def get_remaining_time_in_millis(self) -> int:
1515
return 1000
1616

17+
1718
def test_publish_event_with_synchronous_resolver():
1819
"""Test handling a publish event with a synchronous resolver."""
1920
# GIVEN a sample publish event

examples/event_handler_appsync_events/src/getting_started_with_testing_subscribe.py

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def __init__(self):
1414
def get_remaining_time_in_millis(self) -> int:
1515
return 1000
1616

17+
1718
def test_subscribe_event_with_valid_return():
1819
"""Test error handling during publish event processing."""
1920
# GIVEN a sample publish event
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,47 @@
11
{
2-
"identity":"None",
3-
"result":"None",
4-
"request":{
5-
"headers": {
6-
"x-forwarded-for": "1.1.1.1, 2.2.2.2",
7-
"cloudfront-viewer-country": "US",
8-
"cloudfront-is-tablet-viewer": "false",
9-
"via": "2.0 xxxxxxxxxxxxxxxx.cloudfront.net (CloudFront)",
10-
"cloudfront-forwarded-proto": "https",
11-
"origin": "https://us-west-1.console.aws.amazon.com",
12-
"content-length": "217",
13-
"accept-language": "en-US,en;q=0.9",
14-
"host": "xxxxxxxxxxxxxxxx.appsync-api.us-west-1.amazonaws.com",
15-
"x-forwarded-proto": "https",
16-
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36",
17-
"accept": "*/*",
18-
"cloudfront-is-mobile-viewer": "false",
19-
"cloudfront-is-smarttv-viewer": "false",
20-
"accept-encoding": "gzip, deflate, br",
21-
"referer": "https://us-west-1.console.aws.amazon.com/appsync/home?region=us-west-1",
22-
"content-type": "application/json",
23-
"sec-fetch-mode": "cors",
24-
"x-amz-cf-id": "3aykhqlUwQeANU-HGY7E_guV5EkNeMMtwyOgiA==",
25-
"x-amzn-trace-id": "Root=1-5f512f51-fac632066c5e848ae714",
26-
"authorization": "eyJraWQiOiJScWFCSlJqYVJlM0hrSnBTUFpIcVRXazNOW...",
27-
"sec-fetch-dest": "empty",
28-
"x-amz-user-agent": "AWS-Console-AppSync/",
29-
"cloudfront-is-desktop-viewer": "true",
30-
"sec-fetch-site": "cross-site",
31-
"x-forwarded-port": "443"
2+
"identity":"None",
3+
"result":"None",
4+
"request":{
5+
"headers": {
6+
"x-forwarded-for": "1.1.1.1, 2.2.2.2",
7+
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36",
8+
},
9+
"domainName":"None"
10+
},
11+
"info":{
12+
"channel":{
13+
"path":"/default/channel",
14+
"segments":[
15+
"default",
16+
"channel"
17+
]
3218
},
33-
"domainName":"None"
34-
},
35-
"info":{
36-
"channel":{
37-
"path":"/default/channel",
38-
"segments":[
39-
"default",
40-
"channel"
41-
]
42-
},
43-
"channelNamespace":{
44-
"name":"default"
45-
},
46-
"operation":"PUBLISH"
47-
},
48-
"error":"None",
49-
"prev":"None",
50-
"stash":{
51-
52-
},
53-
"outErrors":[
54-
55-
],
56-
"events":[
57-
{
58-
"payload":{
59-
"data":"data_1"
60-
},
61-
"id":"1"
62-
},
63-
{
64-
"payload":{
65-
"data":"data_2"
66-
},
67-
"id":"2"
68-
}
69-
]
70-
}
19+
"channelNamespace":{
20+
"name":"default"
21+
},
22+
"operation":"PUBLISH"
23+
},
24+
"error":"None",
25+
"prev":"None",
26+
"stash":{
27+
28+
},
29+
"outErrors":[
30+
31+
],
32+
"events":[
33+
{
34+
"payload":{
35+
"data":"data_1"
36+
},
37+
"id":"1"
38+
},
39+
{
40+
"payload":{
41+
"data":"data_2"
42+
},
43+
"id":"2"
44+
}
45+
]
46+
}
7147

Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
{
2-
"events":[
3-
{
4-
"payload":{
5-
"data":"data_1"
6-
},
7-
"id":"1"
8-
},
9-
{
10-
"payload":{
11-
"data":"data_2"
12-
},
13-
"id":"2"
14-
}
15-
]
16-
}
2+
"events":[
3+
{
4+
"payload":{
5+
"data":"data_1"
6+
},
7+
"id":"1"
8+
},
9+
{
10+
"payload":{
11+
"data":"data_2"
12+
},
13+
"id":"2"
14+
}
15+
]
16+
}
1717

Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"error": "Exception - An exception occurred"
3-
}
2+
"error": "Exception - An exception occurred"
3+
}
44

0 commit comments

Comments
 (0)