Event Handler for AWS AppSync real-time events.
stateDiagram-v2
direction LR
EventSource: AppSync Events
EventHandlerResolvers: Publish & Subscribe events
LambdaInit: Lambda invocation
EventHandler: Event Handler
EventHandlerResolver: Route event based on namespace/channel
YourLogic: Run your registered handler function
EventHandlerResolverBuilder: Adapts response to AppSync contract
LambdaResponse: Lambda response
state EventSource {
EventHandlerResolvers
}
EventHandlerResolvers --> LambdaInit
LambdaInit --> EventHandler
EventHandler --> EventHandlerResolver
state EventHandler {
[*] --> EventHandlerResolver: app.resolve(event, context)
EventHandlerResolver --> YourLogic
YourLogic --> EventHandlerResolverBuilder
}
EventHandler --> LambdaResponse
Key Features¶
AWS AppSync Events. A service that enables you to quickly build secure, scalable real-time WebSocket APIs without managing infrastructure or writing API code. It handles connection management, message broadcasting, authentication, and monitoring, reducing time to market and operational costs.
Getting started¶ Tip: New to AppSync Real-time API?Visit AWS AppSync Real-time documentation to understand how to set up subscriptions and pub/sub messaging.
Required resources¶You must have an existing AppSync Events API with real-time capabilities enabled and IAM permissions to invoke your Lambda function.
Getting started with AppSync Events
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
Resources:
WebsocketAPI:
Type: AWS::AppSync::Api
Properties:
EventConfig:
AuthProviders:
- AuthType: API_KEY
ConnectionAuthModes:
- AuthType: API_KEY
DefaultPublishAuthModes:
- AuthType: API_KEY
DefaultSubscribeAuthModes:
- AuthType: API_KEY
Name: RealTimeEventAPI
WebasocketApiKey:
Type: AWS::AppSync::ApiKey
Properties:
ApiId: !GetAtt WebsocketAPI.ApiId
Description: "API KEY"
Expires: 365
WebsocketAPINamespace:
Type: AWS::AppSync::ChannelNamespace
Properties:
ApiId: !GetAtt WebsocketAPI.ApiId
Name: powertools
AppSync request and response format¶
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.
AppSync payload requestAppSync payload responseAppSync payload response with error
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
{
"identity":"None",
"result":"None",
"request":{
"headers": {
"x-forwarded-for": "1.1.1.1, 2.2.2.2",
"cloudfront-viewer-country": "US",
"cloudfront-is-tablet-viewer": "false",
"via": "2.0 xxxxxxxxxxxxxxxx.cloudfront.net (CloudFront)",
"cloudfront-forwarded-proto": "https",
"origin": "https://us-west-1.console.aws.amazon.com",
"content-length": "217",
"accept-language": "en-US,en;q=0.9",
"host": "xxxxxxxxxxxxxxxx.appsync-api.us-west-1.amazonaws.com",
"x-forwarded-proto": "https",
"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",
"accept": "*/*",
"cloudfront-is-mobile-viewer": "false",
"cloudfront-is-smarttv-viewer": "false",
"accept-encoding": "gzip, deflate, br",
"referer": "https://us-west-1.console.aws.amazon.com/appsync/home?region=us-west-1",
"content-type": "application/json",
"sec-fetch-mode": "cors",
"x-amz-cf-id": "3aykhqlUwQeANU-HGY7E_guV5EkNeMMtwyOgiA==",
"x-amzn-trace-id": "Root=1-5f512f51-fac632066c5e848ae714",
"authorization": "eyJraWQiOiJScWFCSlJqYVJlM0hrSnBTUFpIcVRXazNOW...",
"sec-fetch-dest": "empty",
"x-amz-user-agent": "AWS-Console-AppSync/",
"cloudfront-is-desktop-viewer": "true",
"sec-fetch-site": "cross-site",
"x-forwarded-port": "443"
},
"domainName":"None"
},
"info":{
"channel":{
"path":"/default/channel",
"segments":[
"default",
"channel"
]
},
"channelNamespace":{
"name":"default"
},
"operation":"PUBLISH"
},
"error":"None",
"prev":"None",
"stash":{
},
"outErrors":[
],
"events":[
{
"payload":{
"data":"data_1"
},
"id":"1"
},
{
"payload":{
"data":"data_2"
},
"id":"2"
}
]
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
{
"events":[
{
"payload":{
"data":"data_1"
},
"id":"1"
},
{
"payload":{
"data":"data_2"
},
"id":"2"
}
]
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14
{
"events":[
{
"error": "Error message",
"id":"1"
},
{
"payload":{
"data":"data_2"
},
"id":"2"
}
]
}
Events response with error¶
When processing events with Lambda, you can return errors to AppSync in three ways:
error
key within each individual item's response. AppSync Events expects this format for item-specific errors.error
key. This signals a general failure, and AppSync treats the entire request as unsuccessful.When you return Resolve
or ResolveAsync
from your handler it will automatically parse the incoming event data and invokes the appropriate handler based on the namespace/channel pattern you register.
You can define your handlers for different event types using the OnPublish()
, OnPublishAggregate()
, and OnSubscribe()
methods and their Async
versions OnPublishAsync()
, OnPublishAggregateAsync()
, and OnSubscribeAsync()
.
Publish events - Class library handlerPublish events - Executable assembly handlersSubscribe to events
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
using AWS.Lambda.Powertools.EventHandler.AppSyncEvents;
public class Function
{
AppSyncEventsResolver _app;
public Function()
{
_app = new AppSyncEventsResolver();
_app.OnPublishAsync("/default/channel", async (payload) =>
{
// Handle events or
// return unchanged payload
return payload;
});
}
public async Task<AppSyncEventsResponse> FunctionHandler(AppSyncEventsRequest input, ILambdaContext context)
{
return await _app.ResolveAsync(input, context);
}
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
using AWS.Lambda.Powertools.EventHandler.AppSyncEvents;
var app = new AppSyncEventsResolver();
app.OnPublishAsync("/default/channel", async (payload) =>
{
// Handle events or
// return unchanged payload
return payload;
}
async Task<AppSyncEventsResponse> Handler(AppSyncEventsRequest appSyncEvent, ILambdaContext context)
{
return await app.ResolveAsync(appSyncEvent, context);
}
await LambdaBootstrapBuilder.Create((Func<AppSyncEventsRequest, ILambdaContext, Task<AppSyncEventsResponse>>)Handler,
new DefaultLambdaJsonSerializer())
.Build()
.RunAsync();
app.OnSubscribe("/default/*", (payload) =>
{
// Handle subscribe events
// return true to allow subscription
// return false or throw to reject subscription
return true;
});
Advanced¶ Wildcard patterns and handler precedence¶
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.
When an event matches with multiple handlers, the most specific pattern takes precedence.
Wildcard patterns
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
app.OnPublish("/default/channel1", (payload) =>
{
// This handler will be called for events on /default/channel1
return payload;
});
app.OnPublish("/default/*", (payload) =>
{
// This handler will be called for all channels in the default namespace
// EXCEPT for /default/channel1 which has a more specific handler
return payload;
});
app.OnPublish("/*", (payload) =>
{
# This handler will be called for all channels in all namespaces
# EXCEPT for those that have more specific handlers
return payload;
});
Supported wildcard patterns
Only the following patterns are supported:
/namespace/*
- Matches all channels in the specified namespace/*
- Matches all channels in all namespacesPatterns like /namespace/channel*
or /namespace/*/subpath
are not supported.
More specific routes will always take precedence over less specific ones. For example, /default/channel1
will take precedence over /default/*
, which will take precedence over /*
.
OnPublishAggregate()
and OnPublishAggregateAsync()
, receives a list of all events, requiring you to manage the response format. Ensure your response includes results for each event in the expected AppSync Request and Response Format.
In some scenarios, you might want to process all events for a channel as a batch rather than individually. This is useful when you need to:
Aggregated processing
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
app.OnPublishAggregate("/default/channel", (payload) =>
{
var evt = new List<AppSyncEvent>();
foreach (var item in payload.Events)
{
if (item.Payload["eventType"].ToString() == "data_2")
{
pd.Payload["message"] = "Hello from /default/channel2 with data_2";
pd.Payload["data"] = new Dictionary<string, object>
{
{ "key", "value" }
};
}
evt.Add(pd);
}
return new AppSyncEventsResponse
{
Events = evt
};
});
Handling errors¶
You can filter or reject events by raising exceptions in your resolvers or by formatting the payload according to the expected response structure. This instructs AppSync not to propagate that specific message, so subscribers will not receive it.
Handling errors with individual items¶When processing items individually with OnPublish()
and OnPublishAsync()
, you can raise an exception to fail a specific item. When an exception is raised, the Event Handler will catch it and include the exception name and message in the response.
Error handling individual itemsError handling individual items AsyncError handling individual items response
app.OnPublish("/default/channel", (payload) =>
{
throw new Exception("My custom exception");
});
app.OnPublishAsync("/default/channel", async (payload) =>
{
throw new Exception("My custom exception");
});
1 2 3 4 5 6 7 8 9 10 11 12 13 14
{
"events":[
{
"error": "My custom exception",
"id":"1"
},
{
"payload":{
"data":"data_2"
},
"id":"2"
}
]
}
Handling errors with batch of items¶
When processing batch of items with OnPublishAggregate()
and OnPublishAggregateAsync()
, you must format the payload according the expected response.
Error handling batch itemsError handling batch items AsyncError handling batch items response
app.OnPublishAggregate("/default/channel", (payload) =>
{
throw new Exception("My custom exception");
});
app.OnPublishAggregateAsync("/default/channel", async (payload) =>
{
throw new Exception("My custom exception");
});
{
"error": "My custom exception"
}
Raising UnauthorizedException
will cause the Lambda invocation to fail.
You can also reject the entire payload by raising an UnauthorizedException
. This prevents Powertools for AWS from processing any messages and causes the Lambda invocation to fail, returning an error to AppSync.
Rejecting the entire request
app.OnPublish("/default/channel", (payload) =>
{
throw new UnauthorizedException("My custom exception");
});
Accessing Lambda context and event¶
You can access to the original Lambda event or context for additional information. These are accessible via the app instance:
Accessing Lambda context
app.OnPublish("/default/channel", (payload, ctx) =>
{
payload["functionName"] = ctx.FunctionName;
return payload;
});
Event Handler workflow¶ Working with single items¶
sequenceDiagram
participant Client
participant AppSync
participant Lambda
participant EventHandler
note over Client,EventHandler: Individual Event Processing (aggregate=False)
Client->>+AppSync: Send multiple events to channel
AppSync->>+Lambda: Invoke Lambda with batch of events
Lambda->>+EventHandler: Process events with aggregate=False
loop For each event in batch
EventHandler->>EventHandler: Process individual event
end
EventHandler-->>-Lambda: Return array of processed events
Lambda-->>-AppSync: Return event-by-event responses
AppSync-->>-Client: Report individual event statuses
Working with aggregated items¶
sequenceDiagram
participant Client
participant AppSync
participant Lambda
participant EventHandler
note over Client,EventHandler: Aggregate Processing Workflow
Client->>+AppSync: Send multiple events to channel
AppSync->>+Lambda: Invoke Lambda with batch of events
Lambda->>+EventHandler: Process events with aggregate=True
EventHandler->>EventHandler: Batch of events
EventHandler->>EventHandler: Process entire batch at once
EventHandler->>EventHandler: Format response for each event
EventHandler-->>-Lambda: Return aggregated results
Lambda-->>-AppSync: Return success responses
AppSync-->>-Client: Confirm all events processed
Authorization fails for publish¶
sequenceDiagram
participant Client
participant AppSync
participant Lambda
participant EventHandler
note over Client,EventHandler: Publish Event Authorization Flow
Client->>AppSync: Publish message to channel
AppSync->>Lambda: Invoke Lambda with publish event
Lambda->>EventHandler: Process publish event
alt Authorization Failed
EventHandler->>EventHandler: Authorization check fails
EventHandler->>Lambda: Raise UnauthorizedException
Lambda->>AppSync: Return error response
AppSync--xClient: Message not delivered
AppSync--xAppSync: No distribution to subscribers
else Authorization Passed
EventHandler->>Lambda: Return successful response
Lambda->>AppSync: Return processed event
AppSync->>Client: Acknowledge message
AppSync->>AppSync: Distribute to subscribers
end
Authorization fails for subscribe¶
sequenceDiagram
participant Client
participant AppSync
participant Lambda
participant EventHandler
note over Client,EventHandler: Subscribe Event Authorization Flow
Client->>AppSync: Request subscription to channel
AppSync->>Lambda: Invoke Lambda with subscribe event
Lambda->>EventHandler: Process subscribe event
alt Authorization Failed
EventHandler->>EventHandler: Authorization check fails
EventHandler->>Lambda: Raise UnauthorizedException
Lambda->>AppSync: Return error response
AppSync--xClient: Subscription denied (HTTP 403)
else Authorization Passed
EventHandler->>Lambda: Return successful response
Lambda->>AppSync: Return authorization success
AppSync->>Client: Subscription established
end
Testing your code¶
You can test your event handlers by passing a mocked or actual AppSync Events Lambda event.
Testing publish events¶Test Publish eventsPublish event json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
[Fact]
public void Should_Return_Unchanged_Payload()
{
// Arrange
var lambdaContext = new TestLambdaContext();
var app = new AppSyncEventsResolver();
app.OnPublish("/default/channel", payload =>
{
// Handle channel events
return payload;
});
// Act
var result = app.Resolve(_appSyncEvent, lambdaContext);
// Assert
Assert.Equal("123", result.Events[0].Id);
Assert.Equal("test data", result.Events[0].Payload?["data"].ToString());
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
{
"identity":"None",
"result":"None",
"request":{
"headers": {
"x-forwarded-for": "1.1.1.1, 2.2.2.2",
"cloudfront-viewer-country": "US",
"cloudfront-is-tablet-viewer": "false",
"via": "2.0 xxxxxxxxxxxxxxxx.cloudfront.net (CloudFront)",
"cloudfront-forwarded-proto": "https",
"origin": "https://us-west-1.console.aws.amazon.com",
"content-length": "217",
"accept-language": "en-US,en;q=0.9",
"host": "xxxxxxxxxxxxxxxx.appsync-api.us-west-1.amazonaws.com",
"x-forwarded-proto": "https",
"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",
"accept": "*/*",
"cloudfront-is-mobile-viewer": "false",
"cloudfront-is-smarttv-viewer": "false",
"accept-encoding": "gzip, deflate, br",
"referer": "https://us-west-1.console.aws.amazon.com/appsync/home?region=us-west-1",
"content-type": "application/json",
"sec-fetch-mode": "cors",
"x-amz-cf-id": "3aykhqlUwQeANU-HGY7E_guV5EkNeMMtwyOgiA==",
"x-amzn-trace-id": "Root=1-5f512f51-fac632066c5e848ae714",
"authorization": "eyJraWQiOiJScWFCSlJqYVJlM0hrSnBTUFpIcVRXazNOW...",
"sec-fetch-dest": "empty",
"x-amz-user-agent": "AWS-Console-AppSync/",
"cloudfront-is-desktop-viewer": "true",
"sec-fetch-site": "cross-site",
"x-forwarded-port": "443"
},
"domainName":"None"
},
"info":{
"channel":{
"path":"/default/channel",
"segments":[
"default",
"channel"
]
},
"channelNamespace":{
"name":"default"
},
"operation":"PUBLISH"
},
"error":"None",
"prev":"None",
"stash":{
},
"outErrors":[
],
"events":[
{
"payload":{
"data": "test data"
},
"id":"123"
}
]
}
Testing subscribe events¶
Test Subscribe with code payload mock
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
[Fact]
public async Task Should_Authorize_Subscription()
{
// Arrange
var lambdaContext = new TestLambdaContext();
var app = new AppSyncEventsResolver();
app.OnSubscribeAsync("/default/*", async (info) => true);
var subscribeEvent = new AppSyncEventsRequest
{
Info = new Information
{
Channel = new Channel
{
Path = "/default/channel",
Segments = ["default", "channel"]
},
Operation = AppSyncEventsOperation.Subscribe,
ChannelNamespace = new ChannelNamespace { Name = "default" }
}
};
// Act
var result = await app.ResolveAsync(subscribeEvent, lambdaContext);
// Assert
Assert.Null(result);
}
2025-04-22
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4