Skip to content
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

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants