Skip to content

Commit 3967b15

Browse files
richwolfsebsto
andauthored
Add (API Gateway) WebSockets Support to Swift for AWS Lambda Events (#38)
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: ```javascript { 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: ```swift 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. --------- Co-authored-by: Sébastien Stormacq <[email protected]>
1 parent 318868c commit 3967b15

File tree

2 files changed

+146
-0
lines changed

2 files changed

+146
-0
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) YEARS Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
/// `APIGatewayWebSocketRequest` is a variation of the`APIGatewayV2Request`
16+
/// and contains data coming from the WebSockets API Gateway.
17+
public struct APIGatewayWebSocketRequest: Codable {
18+
/// `Context` contains information to identify the AWS account and resources invoking the Lambda function.
19+
public struct Context: Codable {
20+
public struct Identity: Codable {
21+
public let sourceIp: String
22+
}
23+
24+
public let routeKey: String
25+
public let eventType: String
26+
public let extendedRequestId: String
27+
/// The request time in format: 23/Apr/2020:11:08:18 +0000
28+
public let requestTime: String
29+
public let messageDirection: String
30+
public let stage: String
31+
public let connectedAt: UInt64
32+
public let requestTimeEpoch: UInt64
33+
public let identity: Identity
34+
public let requestId: String
35+
public let domainName: String
36+
public let connectionId: String
37+
public let apiId: String
38+
}
39+
40+
public let headers: HTTPHeaders?
41+
public let multiValueHeaders: HTTPMultiValueHeaders?
42+
public let context: Context
43+
public let body: String?
44+
public let isBase64Encoded: Bool?
45+
46+
enum CodingKeys: String, CodingKey {
47+
case headers
48+
case multiValueHeaders
49+
case context = "requestContext"
50+
case body
51+
case isBase64Encoded
52+
}
53+
}
54+
55+
/// `APIGatewayWebSocketResponse` is a type alias for `APIGatewayV2Request`.
56+
/// Typically, lambda WebSockets servers send clients data via
57+
/// the ApiGatewayManagementApi mechanism. However, APIGateway does require
58+
/// lambda servers to return some kind of status when APIGateway invokes them.
59+
/// This can be as simple as always returning a 200 "OK" response for all
60+
/// WebSockets requests (the ApiGatewayManagementApi can return any errors to
61+
/// WebSockets clients).
62+
public typealias APIGatewayWebSocketResponse = APIGatewayV2Response
63+
64+
#if swift(>=5.6)
65+
extension APIGatewayWebSocketRequest: Sendable {}
66+
extension APIGatewayWebSocketRequest.Context: Sendable {}
67+
extension APIGatewayWebSocketRequest.Context.Identity: Sendable {}
68+
#endif
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) YEARS Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
import Testing
17+
18+
@testable import AWSLambdaEvents
19+
20+
@Suite
21+
class APIGatewayWebSocketsTests {
22+
static let exampleConnectEventBody = """
23+
{
24+
"headers": {
25+
"Host": "lqrlmblaa2.execute-api.us-east-1.amazonaws.com",
26+
"Origin": "wss://lqrlmblaa2.execute-api.us-east-1.amazonaws.com",
27+
"Sec-WebSocket-Extensions": "",
28+
"Sec-WebSocket-Key": "am5ubWVpbHd3bmNyYXF0ag==",
29+
"Sec-WebSocket-Version": "13",
30+
"X-Amzn-Trace-Id": "Root=1-64b83950-42de8e247b4c2b43091ef67c",
31+
"X-Forwarded-For": "24.148.42.16",
32+
"X-Forwarded-Port": "443",
33+
"X-Forwarded-Proto": "https"
34+
},
35+
"multiValueHeaders": {
36+
"Host": [ "lqrlmblaa2.execute-api.us-east-1.amazonaws.com" ],
37+
"Origin": [ "wss://lqrlmblaa2.execute-api.us-east-1.amazonaws.com" ],
38+
"Sec-WebSocket-Extensions": [
39+
"permessage-deflate; client_max_window_bits; server_max_window_bits=15"
40+
],
41+
"Sec-WebSocket-Key": [ "am5ubWVpbHd3bmNyYXF0ag==" ],
42+
"Sec-WebSocket-Version": [ "13" ],
43+
"X-Amzn-Trace-Id": [ "Root=1-64b83950-42de8e247b4c2b43091ef67c" ],
44+
"X-Forwarded-For": [ "24.148.42.16" ],
45+
"X-Forwarded-Port": [ "443" ],
46+
"X-Forwarded-Proto": [ "https" ]
47+
},
48+
"requestContext": {
49+
"routeKey": "$connect",
50+
"eventType": "CONNECT",
51+
"extendedRequestId": "IU3kkGyEoAMFwZQ=",
52+
"requestTime": "19/Jul/2023:19:28:16 +0000",
53+
"messageDirection": "IN",
54+
"stage": "dev",
55+
"connectedAt": 1689794896145,
56+
"requestTimeEpoch": 1689794896162,
57+
"identity": { "sourceIp": "24.148.42.16" },
58+
"requestId": "IU3kkGyEoAMFwZQ=",
59+
"domainName": "lqrlmblaa2.execute-api.us-east-1.amazonaws.com",
60+
"connectionId": "IU3kkeN4IAMCJwA=",
61+
"apiId": "lqrlmblaa2"
62+
},
63+
"isBase64Encoded": false
64+
}
65+
"""
66+
67+
// MARK: - Request -
68+
69+
// MARK: Decoding
70+
@Test func testRequestDecodingExampleConnectRequest() async throws {
71+
let data = APIGatewayWebSocketsTests.exampleConnectEventBody.data(using: .utf8)!
72+
let req = try JSONDecoder().decode(APIGatewayWebSocketRequest.self, from: data)
73+
74+
#expect(req.context.routeKey == "$connect")
75+
#expect(req.context.connectionId == "IU3kkeN4IAMCJwA=")
76+
#expect(req.body == nil)
77+
}
78+
}

0 commit comments

Comments
 (0)