Skip to content

Add (API Gateway) WebSockets Support to Swift for AWS Lambda Events #38

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

Merged
merged 11 commits into from
Jun 30, 2025
Merged
68 changes: 68 additions & 0 deletions Sources/AWSLambdaEvents/APIGateway+WebSockets.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAWSLambdaRuntime open source project
//
// Copyright (c) YEARS Apple Inc. and the SwiftAWSLambdaRuntime project authors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Copyright (c) YEARS Apple Inc. and the SwiftAWSLambdaRuntime project authors
// Copyright (c) 2023 Apple Inc. and the SwiftAWSLambdaRuntime project authors

// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// `APIGatewayWebSocketRequest` is a variation of the`APIGatewayV2Request`
/// and contains data coming from the WebSockets API Gateway.
public struct APIGatewayWebSocketRequest: Codable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can any of this vibe shared with APIGatewayRequest or not worth it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sure ... much of it can be. I just wasn't sure what the correct approach should be. I kind of aimed for a "what's the minimum to make it work" approach. :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably some shared struct they can both include as the underlying implementation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it would be helpful, I would be happy to look at the APIGatewayV2Request event, as well as my proposed APIGatewayWebSocketRequest event and extract those items shared between them as a struct both can use…and then update this pull request.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it would be helpful, I would be happy to look at the APIGatewayV2Request event, as well as my proposed APIGatewayWebSocketRequest event and extract those items shared between them as a struct both can use…and then update this pull request.

/// `Context` contains information to identify the AWS account and resources invoking the Lambda function.
public struct Context: Codable {
public struct Identity: Codable {
public let sourceIp: String
}

public let routeKey: String
public let eventType: String
public let extendedRequestId: String
/// The request time in format: 23/Apr/2020:11:08:18 +0000
public let requestTime: String
public let messageDirection: String
public let stage: String
public let connectedAt: UInt64
public let requestTimeEpoch: UInt64
public let identity: Identity
public let requestId: String
public let domainName: String
public let connectionId: String
public let apiId: String
}

public let headers: HTTPHeaders?
public let multiValueHeaders: HTTPMultiValueHeaders?
Copy link
Contributor

@jsonfry jsonfry Mar 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also see

 public let queryStringParameters: [String: String]?
 public let multiValueQueryStringParameters: [String: [String]]?

come through on a CONNECT event type

public let context: Context
public let body: String?
public let isBase64Encoded: Bool?

enum CodingKeys: String, CodingKey {
case headers
case multiValueHeaders
case context = "requestContext"
case body
case isBase64Encoded
}
}

/// `APIGatewayWebSocketResponse` is a type alias for `APIGatewayV2Request`.
/// Typically, lambda WebSockets servers send clients data via
/// the ApiGatewayManagementApi mechanism. However, APIGateway does require
/// lambda servers to return some kind of status when APIGateway invokes them.
/// This can be as simple as always returning a 200 "OK" response for all
/// WebSockets requests (the ApiGatewayManagementApi can return any errors to
/// WebSockets clients).
public typealias APIGatewayWebSocketResponse = APIGatewayV2Response

#if swift(>=5.6)
extension APIGatewayWebSocketRequest: Sendable {}
extension APIGatewayWebSocketRequest.Context: Sendable {}
extension APIGatewayWebSocketRequest.Context.Identity: Sendable {}
#endif
78 changes: 78 additions & 0 deletions Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAWSLambdaRuntime open source project
//
// Copyright (c) YEARS Apple Inc. and the SwiftAWSLambdaRuntime project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Foundation
import Testing

@testable import AWSLambdaEvents

@Suite
class APIGatewayWebSocketsTests {
static let exampleConnectEventBody = """
{
"headers": {
"Host": "lqrlmblaa2.execute-api.us-east-1.amazonaws.com",
"Origin": "wss://lqrlmblaa2.execute-api.us-east-1.amazonaws.com",
"Sec-WebSocket-Extensions": "",
"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
}
"""

// MARK: - Request -

// MARK: Decoding
@Test func testRequestDecodingExampleConnectRequest() async throws {
let data = APIGatewayWebSocketsTests.exampleConnectEventBody.data(using: .utf8)!
let req = try JSONDecoder().decode(APIGatewayWebSocketRequest.self, from: data)

#expect(req.context.routeKey == "$connect")
#expect(req.context.connectionId == "IU3kkeN4IAMCJwA=")
#expect(req.body == nil)
}
}