swift-aws-lambda-events
swift-aws-lambda-events copied to clipboard
Add (API Gateway) WebSockets Support to Swift for AWS Lambda Events
Add APIGateway WebSockets Event Type
Motivation:
What I propose is adding WebSockets support to AWS Lambda Events.
Let me begin by stating outright that I am not sure this is the correct approach to take to bring WebSockets to AWS Lambda Events. Therefore, if this pull request is outright rejected, it won't hurt my feelings in the slightest.
API Gateway supports not only RESTful APIs, but also WebSockets. The way that it works is that API Gateway manages WebSockets sessions with clients. Whenever a client sends API Gateway some WebSockets data, API Gateway bundles it up in as an APIGatewayV2 request (at least, according to Amazon) and passes it along to a designated target…usually a Lambda function. This is what a bundled request looks like:
{
headers: {
Host: 'lqrlmblaa2.execute-api.us-east-1.amazonaws.com',
Origin: 'wss://lqrlmblaa2.execute-api.us-east-1.amazonaws.com',
'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits; server_max_window_bits=15',
'Sec-WebSocket-Key': 'am5ubWVpbHd3bmNyYXF0ag==',
'Sec-WebSocket-Version': '13',
'X-Amzn-Trace-Id': 'Root=1-64b83950-42de8e247b4c2b43091ef67c',
'X-Forwarded-For': '24.148.42.16',
'X-Forwarded-Port': '443',
'X-Forwarded-Proto': 'https'
},
multiValueHeaders: {
Host: [ 'lqrlmblaa2.execute-api.us-east-1.amazonaws.com' ],
Origin: [ 'wss://lqrlmblaa2.execute-api.us-east-1.amazonaws.com' ],
'Sec-WebSocket-Extensions': [
'permessage-deflate; client_max_window_bits; server_max_window_bits=15'
],
'Sec-WebSocket-Key': [ 'am5ubWVpbHd3bmNyYXF0ag==' ],
'Sec-WebSocket-Version': [ '13' ],
'X-Amzn-Trace-Id': [ 'Root=1-64b83950-42de8e247b4c2b43091ef67c' ],
'X-Forwarded-For': [ '24.148.42.16' ],
'X-Forwarded-Port': [ '443' ],
'X-Forwarded-Proto': [ 'https' ]
},
requestContext: {
routeKey: '$connect',
eventType: 'CONNECT',
extendedRequestId: 'IU3kkGyEoAMFwZQ=',
requestTime: '19/Jul/2023:19:28:16 +0000',
messageDirection: 'IN',
stage: 'dev',
connectedAt: 1689794896145,
requestTimeEpoch: 1689794896162,
identity: { sourceIp: '24.148.42.16' },
requestId: 'IU3kkGyEoAMFwZQ=',
domainName: 'lqrlmblaa2.execute-api.us-east-1.amazonaws.com',
connectionId: 'IU3kkeN4IAMCJwA=',
apiId: 'lqrlmblaa2'
},
isBase64Encoded: false
}
The problem, of course, is that the current APIGatewayV2Request type cannot decode that JSON because it is is missing a number of non-optional data values that APIGatewayV2Request expects to exist (e.g., version, rawPath, etc.).
There are (at least as far as I can tell) two solutions to make this work. The first is simply to alter the current APIGatewayV2Request so that a number of its data values become optionals. I resisted suggesting this because I suspected it could easily break production code (forcing developers to if-let things). I thought a better solution might simply be to create a new request/response type pair that could accommodate WebSockets APIs.
Modifications:
I suggest adding a new event source file to AWS Lambda Events: APIGateway+WebSockets.swift containing two new types: APIGatewayWebSocketRequest and APIGatewayWebSocketResponse. APIGatewayWebSocketResponse would simply be a type alias (since responses require that no changes be made to that type); APIGatewayWebSocketRequest would be capable of decoding the JSON listed above.
A typical Lambda handler supporting WebSockets would look like this:
func handle(
_ request: APIGatewayWebSocketRequest,
context: LambdaContext
) async throws -> APIGatewayWebSocketResponse {
let connectionID = request.context.connectionId
let routeKey = request.context.routeKey
// Route based on the type of WebSockets request
// The following are "default" request types
switch routeKey {
case "$connect": break
case "$disconnect": break
case "$default":
if let body = request.body {
// Responses are sent to clients via the
// ApiGatewayManagementApi. "post" is a method
// (not shown) which does that
try await post(
message: "{\"echo\": \"\(body)\"}",
toConnectionWithID: connectionID
)
}
default:
logger.log(level: .info, "Something weird happened");
}
// API Gateway requires that "some" status be returned
// "no matter what"
return APIGatewayWebSocketResponse(statusCode: .ok)
}
Note that responses to WebSockets clients (including, potentially, errors) are made through Amazon's ApiGatewayManagementApi. However, API Gateway itself always expects some kind of response…this can be a simple as always sending a 200 "OK" back to API Gateway.
Result:
The Swift for AWS Lambda Runtime would be able to support API Gateway WebSockets applications.
I think this is a fine approach. @dave-moser @sebsto opinions?
@tomerd @richwolf apologies for my late feedback as I am just back from PTO.
I agree with @richwolf approach to not modify APIGatewayV2Request to support web socket. I am not too concerned about breaking existing apps, as this project is still in version 0.x and developers expect things to change before v1.0. I am more concerned that this would alter the semantic of some fields (like rawPath) that can not be null for REST API. Having this as an optional to support web sockets will oblige REST API developers to manage optional where it is not needed and not desirable.
My approach would be to factor out all common elements between APIGatewayv2Request and APIGatewayWebSocketRequest (maybe a common protocol that both structs implement) to avoid code duplication.
(and the same for the Response structs)
Thank you Rich for proposing this PR.
@tomerd @richwolf apologies for my late feedback as I am just back from PTO.
No worries! I hope all is good on your end.
My approach would be to factor out all common elements between
APIGatewayv2RequestandAPIGatewayWebSocketRequest(maybe a common protocol that both structs implement) to avoid code duplication.(and the same for the
Responsestructs) optional where it is not needed and not desirable.
I don't want to step on any coding toes ... would you all like me to work on that? I am happy to defer to others if that would be best ... or proceed along those lines if that would free up everyone's time.
I'm planning on handling some websocket events soonish this year, would love to see this progressed and happy to help if I can?
I'll need to make something work regardless of this PR, but obviously it's better to have something that works for all users of this library.
My apologies @jsonfry…I think this is on me. I've been meaning to return to this at some point but never got to it. I think what the group wanted was to see if it's possible to have a protocol that factored out code common to all flavors of the API Gateway v2 request. I started that work, just never finished it. Lemme see if I can fix it up in the next couple of days and update this PR.
Thank you so much!
@tomerd, @sebsto…in catching back up with this, I notice that APIGatewayLambdaAuthorizers have been added to the family of Swift Lambda event types (I'm assuming they're a V2 event type)…they allow custom Swift Lambda authorizers to be coded for APIs. The lambda authorizer event shares many of same properties that the straight-up V2 and V2 WebSockets requests do. Do you want me to include Lambda authorizer events in the attempt to extract commonality between all V2 request types? My guess would be "yes"…but it means kind of expanding this PR a bit.
Do you want me to include Lambda authorizer events in the attempt to extract commonality between all V2 request types? My guess would be "yes"…but it means kind of expanding this PR a bit.
Yes :D consider splitting to multiple PRs to make it easier to review and make progress
Yes :D consider splitting to multiple PRs to make it easier to review and make progress
Oh wow, that's really sage advice! Note to self: always try to make the reviewers' lives simpler. :) I'll follow up with a separate PR for APIGatewayLambdaAuthorizers.