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

Conversation

richwolf
Copy link
Contributor

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.

@tomerd
Copy link
Contributor

tomerd commented Jul 26, 2023

I think this is a fine approach. @dave-moser @sebsto opinions?

//
// 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


/// `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.

@sebsto
Copy link
Contributor

sebsto commented Aug 24, 2023

@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.

@richwolf
Copy link
Contributor Author

@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 APIGatewayv2Request and APIGatewayWebSocketRequest (maybe a common protocol that both structs implement) to avoid code duplication.

(and the same for the Response structs)
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.

@jsonfry
Copy link
Contributor

jsonfry commented Mar 5, 2024

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.

@richwolf
Copy link
Contributor Author

richwolf commented Mar 5, 2024

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.

@jsonfry
Copy link
Contributor

jsonfry commented Mar 6, 2024

Thank you so much!

@richwolf
Copy link
Contributor Author

richwolf commented Mar 8, 2024

@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.

@tomerd
Copy link
Contributor

tomerd commented Mar 12, 2024

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

@richwolf
Copy link
Contributor Author

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.

}

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

@sebsto sebsto added the 🆕 semver/minor Adds new public API. label Nov 26, 2024
@sebsto
Copy link
Contributor

sebsto commented Dec 19, 2024

@richwolf Now that we have Lambda Authorizers supported (see #42) - how do you ant to proceed with this PR ?

@richwolf
Copy link
Contributor Author

richwolf commented Dec 19, 2024

@richwolf Now that we have Lambda Authorizers supported (see #42) - how do you ant to proceed with this PR ?

Hi @sebsto !!

Couple things ...

First, my sincere apologies for letting this sit for so long. I truly regret not following up on it. I really should have. I would make a terrible AWS employee! :)

Nextly, I am less familiar with Lambda authorizers for API Gateway ... but ... if I am understanding authorizers (in general) correctly ... say a developer were to choose "Cognito" as an authorizer for an resource/method in API Gateway (instead of "Lambda"), then wouldn't that still require what's in this PR ... or, generally speaking, the "traditional" V1 and V2 payloads?

But ... and perhaps you would know more ... if AWS were deprecating "Cognito" as an authorizer choice in API Gateway, preferring "Lambda" in future development efforts, then I could see how it would make sense to focus on that. I would say, ultimately, it's really up to folks like you and @tomerd ... but my gut instinct is that the "traditional" V1 and V2 payloads would need to continue to supported to the fullest extent possible. If there are fields missing, my guess is that they should be added.

But I wholeheartedly defer to your wisdom here.

@sebsto
Copy link
Contributor

sebsto commented Jun 27, 2025

Hey @richwolf Thank you for keeping this updated.
I would like to proceed and merge this PR, it has been open since too long :-)

  • Is there any pending question that I can help to answer (we had a discussion about Lambda vs Cognito authorizer but I don't see any conclusion / decision). Cognito authorizer are not going to disapear.
  • Is this PR working with the Lambda v2 (work in progres) aka the main branch of the Lambda runtime ?
  • Can you merge the main branch into your branch to keep it up-to-date with other changes ?

What are the remaining questions / doubts / decisions to take to close the work on this ?

@richwolf
Copy link
Contributor Author

richwolf commented Jun 28, 2025

@sebsto…actually, I feel it is I that owe you an apology here. I've let this sit idle for way too long. I started getting back into this mainly because I submitted this (WebSockets and Swift) as a talk idea for ServerSideSwift 2025…I wanted something to motivate me to get back into working on servers and Swift. So the main question I would have going forward is whether to try to implement what @tomerd asked about earlier…consolidating what is common amongst API Gateway Lambda integrations…v2, WebSockets. I had experimented with creating a struct…APIGatewayLambdaIntegrationConforming or APIGatewayContextConforming…to sort of coalesce what is common among all API Gateway Lambda integration varieties…especially what's in the context…but I am not sure that is the right approach. I am beginning to think that maybe the right thing to do is to withdraw this pull request until that's worked though so that this isn't left hanging here in the interim. I still think it's correct for AWS Lambda Events to cover WebSockets. I will follow your lead on this…if you would like me to try to do what @tomerd asked, and keep this PR open, I can try for that. Of if you would like me to do something else, that's also cool. I do totally get that you wanna resolve this "somehow". 😄

@sebsto sebsto self-assigned this Jun 29, 2025
@sebsto sebsto requested a review from Copilot June 29, 2025 09:16
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Adds support for API Gateway WebSocket events to the Swift AWS Lambda Events library, enabling Lambda functions to decode and respond to WebSocket frames.

  • Introduces APIGatewayWebSocketRequest for decoding WebSocket event payloads.
  • Defines APIGatewayWebSocketResponse as an alias to APIGatewayV2Response.
  • Adds a unit test for decoding a $connect WebSocket event.

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
Sources/AWSLambdaEvents/APIGateway+WebSockets.swift Defines the WebSocket request struct, its nested types, and the response alias.
Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift Adds a test case for decoding an example connect event.
Comments suppressed due to low confidence (4)

Sources/AWSLambdaEvents/APIGateway+WebSockets.swift:15

  • [nitpick] There’s a missing space between 'the' and the backticked type, and the phrase could be clarified. Consider changing to: '... is a variation of the APIGatewayV2Request.'
/// `APIGatewayWebSocketRequest` is a variation of the`APIGatewayV2Request`

Sources/AWSLambdaEvents/APIGateway+WebSockets.swift:55

  • The comment references APIGatewayV2Request, but the alias actually targets APIGatewayV2Response. Please update the comment to match the correct type.
/// `APIGatewayWebSocketResponse` is a type alias for `APIGatewayV2Request`.

Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift:75

  • Consider adding a complementary test case where the JSON contains a non-null body value to verify that APIGatewayWebSocketRequest.body is decoded correctly.
        XCTAssertNil(req?.body)

Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift:1

  • [nitpick] The file name uses 'Websockets' but the code uses 'WebSocket'. For consistency, consider renaming the file to 'APIGateway+WebSocketsTests.swift'.
//===----------------------------------------------------------------------===//

@sebsto
Copy link
Contributor

sebsto commented Jun 29, 2025

It would be great to meet you at Swift Server Conf !
I think it's important to support WebSocket.

I would suggest to move on and merge this, then evaluate refactoring opportunities.
Can you fix Swift format ? And migrate the tests to Swift Testing ?
I'm working on the unit tests outside of your code that fail on swift >=6.2.

Here is what my assistant think.


1. HTTP Headers and Body Handling

  • Properties like headers, multiValueHeaders (or just headers in V2), and body, as well as isBase64Encoded, appear in all major APIGateway request/response structs:
    • APIGatewayRequest
    • APIGatewayV2Request
    • APIGatewayWebSocketRequest
    • Their corresponding response types.

2. Context/Identity Structures

  • Each request type defines a nested Context struct (sometimes with further nesting like Identity or HTTP), which contains information about the caller, API, stage, request IDs, etc.
  • Many fields in these context structs overlap, such as apiId, stage, requestId, domainName, and sourceIp.

3. Codable/Sendable Conformance

  • All these types conform to Codable (and, where supported, Sendable).

4. CodingKeys Mapping

  • Several structs use custom CodingKeys enums to map from JSON input to struct properties, especially for context/requestContext fields.

5. Response Struct Pattern

  • The response structs (APIGatewayResponse, APIGatewayV2Response, and by alias, APIGatewayWebSocketResponse) all follow a similar pattern: statusCode, headers, body, and isBase64Encoded, with slight variations.

Candidates for Code Sharing/Abstraction

  • Protocols: Define a protocol for common properties (headers, body, isBase64Encoded) and have each struct conform to it.
    protocol APIGatewayRequestCommon: Codable {
        var headers: HTTPHeaders? { get }
        var body: String? { get }
        var isBase64Encoded: Bool? { get }
    }
  • Shared Context/Identity Structs: Refactor common context/identity fields into base structs or protocols, and compose them in each specific context struct as needed.
  • Initializers/Decoding Helpers: Shared extensions or helper initializers to reduce repeated decoding logic for commonly structured properties.

Summary Table of Common Properties:

Property APIGatewayRequest APIGatewayV2Request APIGatewayWebSocketRequest
headers
multiValueHeaders (V2 drops this)
body
isBase64Encoded
context/requestContext ✓ (as context)
stage ✓ (in context) ✓ (in context) ✓ (in context)
apiId ✓ (in context) ✓ (in context) ✓ (in context)
requestId ✓ (in context) ✓ (in context) ✓ (in context)
domainName ✓ (in context) ✓ (in context) ✓ (in context)
sourceIp ✓ (in context) ✓ (in context) ✓ (in context.identity)

@richwolf
Copy link
Contributor Author

@sebsto…okay, sounds good! And I have my fingers crossed on the WebSockets talk (I also submitted another talk idea for SQLKit). I suppose I'll find out if the SSSwift folks think my ideas are good ones. I think the formatting thing is trivial…so I'll fix that as you suggest. Lastly, I think Copilot has a reasonable idea for consolidating commonality amongst the API Gateway Lambda integration types…but would something similar then need to be done for the other integrations (S3, DynamoDB, etc.)…to be consistent…or is API Gateway basically the "special case"?

@sebsto sebsto self-requested a review June 30, 2025 16:45
@sebsto
Copy link
Contributor

sebsto commented Jun 30, 2025

@richwolf Thank you for your latest changes. I also merged the latest from main, be sure to pull the changes locally before making any further changes.

There are still formatting issues. You can use scripts/format.sh to help you format the code the same way the CI pipeline does.
You can read it's source code here
https://github.com/swift-server/swift-aws-lambda-events/blob/main/scripts/format.sh

Also, would you mind updating your test to use Swift Testing instead of XCTests? I just migrated all the tests to swift Testing, you'll find plenty of examples in the Tests directory.

@sebsto
Copy link
Contributor

sebsto commented Jun 30, 2025

but would something similar then need to be done for the other integrations (S3, DynamoDB, etc.)…to be consistent…or is API Gateway basically the "special case"?

I think APIGateways have more opportunities for code simplifications because the three event payloads are very similar. I'm not sure there is much overlap between other types (S3 and DynamoDB for example)

@richwolf
Copy link
Contributor Author

I think APIGateways have more opportunities for code simplifications because the three event payloads are very similar. I'm not sure there is much overlap between other types (S3 and DynamoDB for example)

Understood. I know less about the other integration requests…but I "suspected" they were all different enough from API Gateway integrations to make it really not super worthwhile. Still, I thought it best to double-check with a pro. 😄

@richwolf
Copy link
Contributor Author

There are still formatting issues. You can use scripts/format.sh to help you format the code the same way the CI pipeline does. You can read it's source code here https://github.com/swift-server/swift-aws-lambda-events/blob/main/scripts/format.sh

Sorry, I changed tabs to spaces, but I think the formatter wanted even more spacing…or it wasn't happy. I let it make the formatting changes it wanted to make.

Also, would you mind updating your test to use Swift Testing instead of XCTests? I just migrated all the tests to swift Testing, you'll find plenty of examples in the Tests directory.

Sure, that makes sense. I will do that.

@sebsto
Copy link
Contributor

sebsto commented Jun 30, 2025

The format.sh script will make the changes on your behalf.

@richwolf
Copy link
Contributor Author

The format.sh script will make the changes on your behalf.

Yeah, I noticed. It should be all good now. 🤞🏻 I think it's all good now. One test failed, but it's not a API Gateway Websockets test.

@sebsto sebsto merged commit 3967b15 into swift-server:main Jun 30, 2025
16 checks passed
@sebsto
Copy link
Contributor

sebsto commented Jun 30, 2025

Merged now, thank you @richwolf for your patience.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🆕 semver/minor Adds new public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants