From f6093fd224e8c7811ae383a86b3230fced49783d Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 30 Nov 2025 18:08:51 -0500 Subject: [PATCH 1/7] Add Streaming + API Gateway --- .../.gitignore | 0 .../Package.swift | 0 Examples/Streaming+APIGateway/README.md | 316 ++++++++++++++++++ .../Sources/main.swift | 0 .../samconfig.toml | 0 Examples/Streaming+APIGateway/template.yaml | 96 ++++++ Examples/Streaming+Codable/README.md | 13 +- Examples/Streaming+FunctionUrl/.gitignore | 8 + Examples/Streaming+FunctionUrl/Package.swift | 27 ++ .../README.md | 13 +- .../Streaming+FunctionUrl/Sources/main.swift | 61 ++++ Examples/Streaming+FunctionUrl/samconfig.toml | 8 + .../template.yaml | 0 13 files changed, 532 insertions(+), 10 deletions(-) rename Examples/{Streaming => Streaming+APIGateway}/.gitignore (100%) rename Examples/{Streaming => Streaming+APIGateway}/Package.swift (100%) create mode 100644 Examples/Streaming+APIGateway/README.md rename Examples/{Streaming => Streaming+APIGateway}/Sources/main.swift (100%) rename Examples/{Streaming => Streaming+APIGateway}/samconfig.toml (100%) create mode 100644 Examples/Streaming+APIGateway/template.yaml create mode 100644 Examples/Streaming+FunctionUrl/.gitignore create mode 100644 Examples/Streaming+FunctionUrl/Package.swift rename Examples/{Streaming => Streaming+FunctionUrl}/README.md (97%) create mode 100644 Examples/Streaming+FunctionUrl/Sources/main.swift create mode 100644 Examples/Streaming+FunctionUrl/samconfig.toml rename Examples/{Streaming => Streaming+FunctionUrl}/template.yaml (100%) diff --git a/Examples/Streaming/.gitignore b/Examples/Streaming+APIGateway/.gitignore similarity index 100% rename from Examples/Streaming/.gitignore rename to Examples/Streaming+APIGateway/.gitignore diff --git a/Examples/Streaming/Package.swift b/Examples/Streaming+APIGateway/Package.swift similarity index 100% rename from Examples/Streaming/Package.swift rename to Examples/Streaming+APIGateway/Package.swift diff --git a/Examples/Streaming+APIGateway/README.md b/Examples/Streaming+APIGateway/README.md new file mode 100644 index 00000000..0548da6f --- /dev/null +++ b/Examples/Streaming+APIGateway/README.md @@ -0,0 +1,316 @@ +# Streaming Lambda function with API Gateway + +You can configure your Lambda function to stream response payloads back to clients through Amazon API Gateway. Response streaming can benefit latency sensitive applications by improving time to first byte (TTFB) performance. This is because you can send partial responses back to the client as they become available. Additionally, you can use response streaming to build functions that return larger payloads. Response stream payloads have a soft limit of 200 MB as compared to the 6 MB limit for buffered responses. Streaming a response also means that your function doesn't need to fit the entire response in memory. For very large responses, this can reduce the amount of memory you need to configure for your function. + +Streaming responses incurs a cost. For more information, see [AWS Lambda Pricing](https://aws.amazon.com/lambda/pricing/). + +You can stream responses through Lambda function URLs, **Amazon API Gateway**, the AWS SDK, or using the Lambda [InvokeWithResponseStream](https://docs.aws.amazon.com/lambda/latest/dg/API_InvokeWithResponseStream.html) API. In this example, we expose the streaming Lambda function through **API Gateway REST API** with response streaming enabled. + +For more information about configuring Lambda response streaming with API Gateway, see [Configure a Lambda proxy integration with payload response streaming](https://docs.aws.amazon.com/apigateway/latest/developerguide/response-streaming-lambda-configure.html). + +## Code + +The sample code creates a `SendNumbersWithPause` struct that conforms to the `StreamingLambdaHandler` protocol provided by the Swift AWS Lambda Runtime. + +The `handle(...)` method of this protocol receives incoming events as a Swift NIO `ByteBuffer` and returns the output as a `ByteBuffer`. + +The response is streamed through the `LambdaResponseStreamWriter`, which is passed as an argument in the `handle` function. + +### Setting HTTP Status Code and Headers + +Before streaming the response body, you can set the HTTP status code and headers using the `writeStatusAndHeaders(_:)` method: + +```swift +try await responseWriter.writeStatusAndHeaders( + StreamingLambdaStatusAndHeadersResponse( + statusCode: 200, + headers: [ + "Content-Type": "text/plain", + "x-my-custom-header": "streaming-example" + ] + ) +) +``` + +The `StreamingLambdaStatusAndHeadersResponse` structure allows you to specify: +- **statusCode**: HTTP status code (e.g., 200, 404, 500) +- **headers**: Dictionary of single-value HTTP headers (optional) + +### Streaming the Response Body + +After setting headers, you can stream the response body by calling the `write(_:)` function of the `LambdaResponseStreamWriter` with partial data repeatedly before finally closing the response stream by calling `finish()`. Developers can also choose to return the entire output and not stream the response by calling `writeAndFinish(_:)`. + +```swift +// Stream data in chunks +for i in 1...3 { + try await responseWriter.write(ByteBuffer(string: "Number: \(i)\n")) + try await Task.sleep(for: .milliseconds(1000)) +} + +// Close the response stream +try await responseWriter.finish() +``` + +An error is thrown if `finish()` is called multiple times or if it is called after having called `writeAndFinish(_:)`. + +### Example Usage Patterns + +The example includes a **SendNumbersWithPause** handler that demonstrates basic streaming with headers, sending numbers with delays + +The `handle(...)` method is marked as `mutating` to allow handlers to be implemented with a `struct`. + +Once the struct is created and the `handle(...)` method is defined, the sample code creates a `LambdaRuntime` struct and initializes it with the handler just created. Then, the code calls `run()` to start the interaction with the AWS Lambda control plane. + +## Build & Package + +To build & archive the package, type the following commands. + +```bash +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingNumbers/StreamingNumbers.zip` + +## Test locally + +You can test the function locally before deploying: + +```bash +swift run + +# In another terminal, test with curl: +curl -v --output response.txt \ + --header "Content-Type: application/json" \ + --data '"this is not used"' \ + http://127.0.0.1:7000/invoke +``` + +## Deploy with AWS SAM + +[AWS SAM](https://aws.amazon.com/serverless/sam/) provides a streamlined way to deploy Lambda functions with API Gateway streaming support. + +**Prerequisites**: Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) + +### SAM Template + +The template file is provided as part of the example in the `template.yaml` file. It defines: + +- A Lambda function with streaming support +- An API Gateway REST API configured for response streaming +- An IAM role that allows API Gateway to invoke the Lambda function with streaming +- The `/stream` endpoint that accepts any HTTP method + +Key configuration details: + +```yaml +Resources: + StreamingNumbers: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingNumbers/StreamingNumbers.zip + Timeout: 60 # Must be bigger than the time it takes to stream the output + Handler: swift.bootstrap + Runtime: provided.al2 + MemorySize: 128 + Architectures: + - arm64 + Events: + StreamingApi: + Type: Api + Properties: + RestApiId: !Ref StreamingApi + Path: /stream + Method: ANY + + StreamingApi: + Type: AWS::Serverless::Api + Properties: + StageName: prod + DefinitionBody: + openapi: "3.0.1" + info: + title: "StreamingAPI" + version: "1.0" + paths: + /stream: + x-amazon-apigateway-any-method: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + # Special URI for streaming invocations + uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2021-11-15/functions/${StreamingNumbers.Arn}/response-streaming-invocations" + timeoutInMillis: 60000 + responseTransferMode: STREAM # Enable streaming + credentials: !GetAtt ApiGatewayLambdaInvokeRole.Arn +``` + +> [!IMPORTANT] +> The timeout value must be bigger than the time it takes for your function to stream its output. Otherwise, the Lambda control plane will terminate the execution environment before your code has a chance to finish writing the stream. The sample function streams responses over 3 seconds, and we set the timeout to 60 seconds for safety. + +### Deploy with SAM + +```bash +sam deploy \ + --resolve-s3 \ + --template-file template.yaml \ + --stack-name StreamingNumbers \ + --capabilities CAPABILITY_IAM +``` + +The API Gateway endpoint URL is provided as part of the output: + +``` +CloudFormation outputs from deployed stack +----------------------------------------------------------------------------------------------------------------------------- +Outputs +----------------------------------------------------------------------------------------------------------------------------- +Key ApiUrl +Description API Gateway endpoint URL for streaming +Value https://abc123xyz.execute-api.us-east-1.amazonaws.com/prod/stream +----------------------------------------------------------------------------------------------------------------------------- +Key LambdaArn +Description Lambda Function ARN +Value arn:aws:lambda:us-east-1:123456789012:function:StreamingNumbers-StreamingNumbers-ABC123 +----------------------------------------------------------------------------------------------------------------------------- +``` + +### Invoke the API Gateway endpoint + +To invoke the streaming API through API Gateway, use `curl` with AWS Sigv4 authentication: + +#### Get AWS Credentials + +Read the [AWS Credentials and Signature](../README.md/#AWS-Credentials-and-Signature) section for more details about the AWS Sigv4 protocol and how to obtain AWS credentials. + +When you have the `aws` command line installed and configured, you will find the credentials in the `~/.aws/credentials` file. + +#### Invoke with authentication + +```bash +# Set your values +API_URL=https://abc123xyz.execute-api.us-east-1.amazonaws.com/prod/stream +REGION=us-east-1 +# Set the AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN environment variables +eval $(aws configure export-credentials --format env) + +# Invoke the streaming API +curl "$API_URL" \ + --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \ + --aws-sigv4 "aws:amz:$REGION:execute-api" \ + -H "x-amz-security-token: $AWS_SESSION_TOKEN" \ + --no-buffer +``` + +> [!NOTE] +> - The `--no-buffer` flag is important for streaming responses - it ensures curl displays data as it arrives +> - The service name for API Gateway is `execute-api` (not `lambda`) +> - If you're not using temporary credentials (session token), you can omit the `x-amz-security-token` header + +This should output the following result, with a one-second delay between each number: + +``` +1 +2 +3 +Streaming complete! +``` + +### Alternative: Test without authentication (not recommended for production) + +If you want to test without authentication, you can modify the API Gateway to use `NONE` auth type. However, this is **not recommended for production** as it exposes your API publicly. + +To enable public access for testing, modify the `template.yaml`: + +```yaml +StreamingApi: + Type: AWS::Serverless::Api + Properties: + StageName: prod + Auth: + DefaultAuthorizer: NONE + # ... rest of configuration +``` + +Then you can invoke without credentials: + +```bash +curl https://abc123xyz.execute-api.us-east-1.amazonaws.com/prod/stream --no-buffer +``` + +### Undeploy with SAM + +When done testing, you can delete the infrastructure with this command: + +```bash +sam delete --stack-name StreamingNumbers +``` + +## Payload decoding + +When you invoke the function through API Gateway, the incoming `ByteBuffer` contains a payload that gives developer access to the underlying HTTP call. The payload contains information about the HTTP verb used, the headers received, the authentication method, and more. + +The [AWS documentation contains the details](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format) of the payload format. The [Swift Lambda Event library](https://github.com/awslabs/swift-aws-lambda-events) contains an [`APIGatewayV2Request` type](https://github.com/awslabs/swift-aws-lambda-events/blob/main/Sources/AWSLambdaEvents/APIGatewayV2.swift) ready to use in your projects. + +Here is an example of API Gateway proxy integration payload: + +```json +{ + "version": "2.0", + "routeKey": "ANY /stream", + "rawPath": "/prod/stream", + "rawQueryString": "", + "headers": { + "accept": "*/*", + "content-length": "0", + "host": "abc123xyz.execute-api.us-east-1.amazonaws.com", + "user-agent": "curl/8.7.1", + "x-amzn-trace-id": "Root=1-67890abc-1234567890abcdef", + "x-forwarded-for": "203.0.113.1", + "x-forwarded-port": "443", + "x-forwarded-proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "abc123xyz", + "domainName": "abc123xyz.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "abc123xyz", + "http": { + "method": "GET", + "path": "/prod/stream", + "protocol": "HTTP/1.1", + "sourceIp": "203.0.113.1", + "userAgent": "curl/8.7.1" + }, + "requestId": "abc123-def456-ghi789", + "routeKey": "ANY /stream", + "stage": "prod", + "time": "30/Nov/2025:10:30:00 +0000", + "timeEpoch": 1733000000000 + }, + "isBase64Encoded": false +} +``` + +## How API Gateway Streaming Works + +When you configure API Gateway with `responseTransferMode: STREAM`: + +1. **Special Lambda URI**: API Gateway uses the `/response-streaming-invocations` endpoint instead of the standard `/invocations` endpoint +2. **InvokeWithResponseStream API**: API Gateway calls the Lambda `InvokeWithResponseStream` API instead of the standard `Invoke` API +3. **Chunked Transfer**: Responses are sent using HTTP chunked transfer encoding, allowing data to flow as it's generated +4. **IAM Permissions**: The API Gateway execution role needs both `lambda:InvokeFunction` and `lambda:InvokeWithResponseStream` permissions + +## ⚠️ Security and Reliability Notice + +These are example applications for demonstration purposes. When deploying such infrastructure in production environments, we strongly encourage you to follow these best practices for improved security and resiliency: + +- **Enable access logging on API Gateway** ([documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)) +- **Configure API Gateway throttling** to protect against abuse ([documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html)) +- **Use AWS WAF** with API Gateway for additional security ([documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-control-access-aws-waf.html)) +- **Ensure Lambda function has concurrent execution limits** ([concurrency documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html), [configuration guide](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html)) +- **Enable encryption for Lambda environment variables** ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html)) +- **Configure a Dead Letter Queue (DLQ)** for Lambda ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq)) +- **Use VPC configuration** when Lambda needs to access private resources ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html), [code example](https://github.com/awslabs/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres)) +- **Implement proper IAM authentication** instead of public access for production APIs +- **Enable CloudWatch Logs** for both API Gateway and Lambda for monitoring and debugging diff --git a/Examples/Streaming/Sources/main.swift b/Examples/Streaming+APIGateway/Sources/main.swift similarity index 100% rename from Examples/Streaming/Sources/main.swift rename to Examples/Streaming+APIGateway/Sources/main.swift diff --git a/Examples/Streaming/samconfig.toml b/Examples/Streaming+APIGateway/samconfig.toml similarity index 100% rename from Examples/Streaming/samconfig.toml rename to Examples/Streaming+APIGateway/samconfig.toml diff --git a/Examples/Streaming+APIGateway/template.yaml b/Examples/Streaming+APIGateway/template.yaml new file mode 100644 index 00000000..659279f1 --- /dev/null +++ b/Examples/Streaming+APIGateway/template.yaml @@ -0,0 +1,96 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for Streaming Example + +# This is an example SAM template for the purpose of this project. +# When deploying such infrastructure in production environment, +# we strongly encourage you to follow these best practices for improved security and resiliency +# - Enable access loggin on API Gateway +# See: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html) +# - Ensure that AWS Lambda function is configured for function-level concurrent execution limit +# See: https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html +# https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html +# - Check encryption settings for Lambda environment variable +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html +# - Ensure that AWS Lambda function is configured for a Dead Letter Queue(DLQ) +# See: https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq +# - Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html +# Code Example: https://github.com/awslabs/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres + +Resources: + # Lambda function with streaming support + StreamingNumbers: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingNumbers/StreamingNumbers.zip + Timeout: 60 # Must be bigger than the time it takes to stream the output + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 128 + Architectures: + - arm64 + Environment: + Variables: + LOG_LEVEL: trace + Events: + StreamingApi: + Type: Api + Properties: + RestApiId: !Ref StreamingApi + Path: /stream + Method: ANY + + # API Gateway REST API with streaming support + StreamingApi: + Type: AWS::Serverless::Api + Properties: + StageName: prod + DefinitionBody: + openapi: "3.0.1" + info: + title: "StreamingAPI" + version: "1.0" + paths: + /stream: + x-amazon-apigateway-any-method: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2021-11-15/functions/${StreamingNumbers.Arn}/response-streaming-invocations" + timeoutInMillis: 60000 + responseTransferMode: STREAM + credentials: !GetAtt ApiGatewayLambdaInvokeRole.Arn + + # IAM Role for API Gateway to invoke Lambda with streaming + ApiGatewayLambdaInvokeRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: apigateway.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: InvokeLambdaWithResponseStream + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - lambda:InvokeFunction + - lambda:InvokeWithResponseStream + Resource: !GetAtt StreamingNumbers.Arn + +Outputs: + # API Gateway endpoint URL + ApiUrl: + Description: API Gateway endpoint URL for streaming + Value: !Sub "https://${StreamingApi}.execute-api.${AWS::Region}.amazonaws.com/prod/stream" + + # Lambda function ARN + LambdaArn: + Description: Lambda Function ARN + Value: !GetAtt StreamingNumbers.Arn diff --git a/Examples/Streaming+Codable/README.md b/Examples/Streaming+Codable/README.md index c5ac2c9f..9edfb316 100644 --- a/Examples/Streaming+Codable/README.md +++ b/Examples/Streaming+Codable/README.md @@ -178,11 +178,11 @@ When you have the `aws` command line installed and configured, you will find the ```bash URL=https://ul3nf4dogmgyr7ffl5r5rs22640fwocc.lambda-url.us-east-1.on.aws/ REGION=us-east-1 -ACCESS_KEY=AK... -SECRET_KEY=... -AWS_SESSION_TOKEN=... -curl --user "${ACCESS_KEY}":"${SECRET_KEY}" \ +# Set the AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN environment variables +eval $(aws configure export-credentials --format env) + +curl --user "${AWS_ACCESS_KEY_ID}":"${AWS_SECRET_ACCESS_KEY}" \ --aws-sigv4 "aws:amz:${REGION}:lambda" \ -H "x-amz-security-token: ${AWS_SESSION_TOKEN}" \ --no-buffer \ @@ -272,9 +272,12 @@ Value https://gaudpin2zjqizfujfnqxstnv6u0czrfu.lambda-url.us-east- Once the function is deployed, you can invoke it with `curl`, similarly to what you did when deploying with the AWS CLI. ```bash +# Set the AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN environment variables +eval $(aws configure export-credentials --format env) + curl -X POST \ --data '{"count": 3, "message": "Hello World!", "delayMs": 1000}' \ - --user "$ACCESS_KEY":"$SECRET_KEY" \ + --user "$AWS_ACCESS_KEY_ID":"$AWS_SECRET_ACCESS_KEY" \ --aws-sigv4 "aws:amz:${REGION}:lambda" \ -H "x-amz-security-token: $AWS_SESSION_TOKEN" \ --no-buffer \ diff --git a/Examples/Streaming+FunctionUrl/.gitignore b/Examples/Streaming+FunctionUrl/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/Examples/Streaming+FunctionUrl/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/Streaming+FunctionUrl/Package.swift b/Examples/Streaming+FunctionUrl/Package.swift new file mode 100644 index 00000000..267668a9 --- /dev/null +++ b/Examples/Streaming+FunctionUrl/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version:6.2 + +import PackageDescription + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "StreamingNumbers", targets: ["StreamingNumbers"]) + ], + dependencies: [ + // For local development (default) + .package(name: "swift-aws-lambda-runtime", path: "../..") + + // For standalone usage, comment the line above and uncomment below: + // .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "1.0.0"), + ], + targets: [ + .executableTarget( + name: "StreamingNumbers", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: "Sources" + ) + ] +) diff --git a/Examples/Streaming/README.md b/Examples/Streaming+FunctionUrl/README.md similarity index 97% rename from Examples/Streaming/README.md rename to Examples/Streaming+FunctionUrl/README.md index 4b2ad39e..dff81d94 100644 --- a/Examples/Streaming/README.md +++ b/Examples/Streaming+FunctionUrl/README.md @@ -160,12 +160,12 @@ When you have the `aws` command line installed and configured, you will find the ```bash URL=https://ul3nf4dogmgyr7ffl5r5rs22640fwocc.lambda-url.us-east-1.on.aws/ REGION=us-east-1 -ACCESS_KEY=AK... -SECRET_KEY=... -AWS_SESSION_TOKEN=... + +# Set the AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN environment variables +eval $(aws configure export-credentials --format env) curl "$URL" \ - --user "${ACCESS_KEY}":"${SECRET_KEY}" \ + --user "${AWS_ACCESS_KEY_ID}":"${AWS_SECRET_ACCESS_KEY}" \ --aws-sigv4 "aws:amz:${REGION}:lambda" \ -H "x-amz-security-token: ${AWS_SESSION_TOKEN}" \ --no-buffer @@ -254,8 +254,11 @@ Value https://gaudpin2zjqizfujfnqxstnv6u0czrfu.lambda-url.us-east- Once the function is deployed, you can invoke it with `curl`, similarly to what you did when deploying with the AWS CLI. ```bash +# Set the AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN environment variables +eval $(aws configure export-credentials --format env) + curl "$URL" \ - --user "$ACCESS_KEY":"$SECRET_KEY" \ + --user "$AWS_ACCESS_KEY_ID":"$AWS_SECRET_ACCESS_KEY" \ --aws-sigv4 "aws:amz:${REGION}:lambda" \ -H "x-amz-security-token: $AWS_SESSION_TOKEN" \ --no-buffer diff --git a/Examples/Streaming+FunctionUrl/Sources/main.swift b/Examples/Streaming+FunctionUrl/Sources/main.swift new file mode 100644 index 00000000..49ff62f2 --- /dev/null +++ b/Examples/Streaming+FunctionUrl/Sources/main.swift @@ -0,0 +1,61 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// 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 AWSLambdaRuntime +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +struct SendNumbersWithPause: StreamingLambdaHandler { + func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + + // Send HTTP status code and headers before streaming the response body + try await responseWriter.writeStatusAndHeaders( + StreamingLambdaStatusAndHeadersResponse( + statusCode: 418, // I'm a tea pot + headers: [ + "Content-Type": "text/plain", + "x-my-custom-header": "streaming-example", + ] + ) + ) + + // Stream numbers with pauses to demonstrate streaming functionality + for i in 1...3 { + // Send partial data + try await responseWriter.write(ByteBuffer(string: "Number: \(i)\n")) + + // Perform some long asynchronous work to simulate processing + try await Task.sleep(for: .milliseconds(1000)) + } + + // Send final message + try await responseWriter.write(ByteBuffer(string: "Streaming complete!\n")) + + // All data has been sent. Close off the response stream. + try await responseWriter.finish() + } +} + +let runtime = LambdaRuntime(handler: SendNumbersWithPause()) +try await runtime.run() diff --git a/Examples/Streaming+FunctionUrl/samconfig.toml b/Examples/Streaming+FunctionUrl/samconfig.toml new file mode 100644 index 00000000..6601b7de --- /dev/null +++ b/Examples/Streaming+FunctionUrl/samconfig.toml @@ -0,0 +1,8 @@ +version = 0.1 +[default.deploy.parameters] +stack_name = "StreamingNumbers" +resolve_s3 = true +s3_prefix = "StreamingNumbers" +region = "us-east-1" +capabilities = "CAPABILITY_IAM" +image_repositories = [] diff --git a/Examples/Streaming/template.yaml b/Examples/Streaming+FunctionUrl/template.yaml similarity index 100% rename from Examples/Streaming/template.yaml rename to Examples/Streaming+FunctionUrl/template.yaml From ea7ffe8b10001f2e4c9e74e4169061a770e014fb Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 30 Nov 2025 18:14:55 -0500 Subject: [PATCH 2/7] add new example to CI and examples README --- .github/workflows/pull_request.yml | 2 +- Examples/README.md | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 8b75be02..7964fef3 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -40,7 +40,7 @@ jobs: # We pass the list of examples here, but we can't pass an array as argument # Instead, we pass a String with a valid JSON array. # The workaround is mentioned here https://github.com/orgs/community/discussions/11692 - examples: "[ 'APIGatewayV1', 'APIGatewayV2', 'APIGatewayV2+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'HelloWorldNoTraits', 'HummingbirdLambda', 'MultiSourceAPI', 'MultiTenant', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'Streaming+Codable', 'ServiceLifecycle+Postgres', 'Testing', 'Tutorial' ]" + examples: "[ 'APIGatewayV1', 'APIGatewayV2', 'APIGatewayV2+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'HelloWorldNoTraits', 'HummingbirdLambda', 'MultiSourceAPI', 'MultiTenant', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming+APIGateway', 'Streaming+FunctionUrl', 'Streaming+Codable', 'ServiceLifecycle+Postgres', 'Testing', 'Tutorial' ]" archive_plugin_examples: "[ 'HelloWorld', 'ResourcesPackaging' ]" archive_plugin_enabled: true diff --git a/Examples/README.md b/Examples/README.md index b17a7271..2096c0ad 100644 --- a/Examples/README.md +++ b/Examples/README.md @@ -38,7 +38,9 @@ This directory contains example code for Lambda functions. - **[S3_Soto](S3_Soto/README.md)**: a Lambda function that uses [Soto](https://github.com/soto-project/soto) to invoke an [Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html) API (requires [AWS SAM](https://aws.amazon.com/serverless/sam/)). -- **[Streaming](Streaming/README.md)**: create a Lambda function exposed as an URL. The Lambda function streams its response over time. (requires [AWS SAM](https://aws.amazon.com/serverless/sam/)). +- **[Streaming with APIGateway](Streaming+APIGateway/README.md)**: create a Lambda function exposed by a REST API Gateway. The Lambda function streams its response over time. (requires [AWS SAM](https://aws.amazon.com/serverless/sam/)). + +- **[Streaming with Function Url](Streaming+FunctionUrl/README.md)**: create a Lambda function exposed as an URL. The Lambda function streams its response over time. (requires [AWS SAM](https://aws.amazon.com/serverless/sam/)). - **[Streaming+Codable](Streaming+Codable/README.md)**: a Lambda function that combines JSON input decoding with response streaming capabilities, demonstrating a streaming codable interface (requires [AWS SAM](https://aws.amazon.com/serverless/sam/) or the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)). From 48bdc4649296f04d4e97d29d0102ebfa52c5c75d Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 30 Nov 2025 18:16:18 -0500 Subject: [PATCH 3/7] fix yaml lint --- Examples/Streaming+APIGateway/template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Streaming+APIGateway/template.yaml b/Examples/Streaming+APIGateway/template.yaml index 659279f1..ea5e3104 100644 --- a/Examples/Streaming+APIGateway/template.yaml +++ b/Examples/Streaming+APIGateway/template.yaml @@ -89,7 +89,7 @@ Outputs: ApiUrl: Description: API Gateway endpoint URL for streaming Value: !Sub "https://${StreamingApi}.execute-api.${AWS::Region}.amazonaws.com/prod/stream" - + # Lambda function ARN LambdaArn: Description: Lambda Function ARN From 716e7a2af6884d5b475f9188199d0f773c84e32d Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 30 Nov 2025 18:26:47 -0500 Subject: [PATCH 4/7] show how to decode the payload --- Examples/Streaming+APIGateway/Package.swift | 3 ++- Examples/Streaming+APIGateway/Sources/main.swift | 6 ++++++ Examples/Streaming+FunctionUrl/Package.swift | 4 +++- Examples/Streaming+FunctionUrl/Sources/main.swift | 6 ++++++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Examples/Streaming+APIGateway/Package.swift b/Examples/Streaming+APIGateway/Package.swift index 267668a9..d3f4006c 100644 --- a/Examples/Streaming+APIGateway/Package.swift +++ b/Examples/Streaming+APIGateway/Package.swift @@ -10,10 +10,11 @@ let package = Package( ], dependencies: [ // For local development (default) - .package(name: "swift-aws-lambda-runtime", path: "../..") + .package(name: "swift-aws-lambda-runtime", path: "../.."), // For standalone usage, comment the line above and uncomment below: // .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "1.0.0"), + .package(url: "https://github.com/awslabs/swift-aws-lambda-events.git", from: "1.0.0"), ], targets: [ .executableTarget( diff --git a/Examples/Streaming+APIGateway/Sources/main.swift b/Examples/Streaming+APIGateway/Sources/main.swift index 49ff62f2..c78c6074 100644 --- a/Examples/Streaming+APIGateway/Sources/main.swift +++ b/Examples/Streaming+APIGateway/Sources/main.swift @@ -13,6 +13,7 @@ // //===----------------------------------------------------------------------===// +import AWSLambdaEvents import AWSLambdaRuntime import NIOCore @@ -29,6 +30,11 @@ struct SendNumbersWithPause: StreamingLambdaHandler { context: LambdaContext ) async throws { + // the payload here is an API Gateway V1 request + // Check the body of the request to extract the business event + let payload = try JSONDecoder().decode(APIGatewayRequest.self, from: Data(event.readableBytesView)) + let _ = payload.body + // Send HTTP status code and headers before streaming the response body try await responseWriter.writeStatusAndHeaders( StreamingLambdaStatusAndHeadersResponse( diff --git a/Examples/Streaming+FunctionUrl/Package.swift b/Examples/Streaming+FunctionUrl/Package.swift index 267668a9..06478d6b 100644 --- a/Examples/Streaming+FunctionUrl/Package.swift +++ b/Examples/Streaming+FunctionUrl/Package.swift @@ -10,10 +10,12 @@ let package = Package( ], dependencies: [ // For local development (default) - .package(name: "swift-aws-lambda-runtime", path: "../..") + .package(name: "swift-aws-lambda-runtime", path: "../.."), // For standalone usage, comment the line above and uncomment below: // .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "1.0.0"), + + .package(url: "https://github.com/awslabs/swift-aws-lambda-events.git", from: "1.0.0"), ], targets: [ .executableTarget( diff --git a/Examples/Streaming+FunctionUrl/Sources/main.swift b/Examples/Streaming+FunctionUrl/Sources/main.swift index 49ff62f2..49e4d63b 100644 --- a/Examples/Streaming+FunctionUrl/Sources/main.swift +++ b/Examples/Streaming+FunctionUrl/Sources/main.swift @@ -13,6 +13,7 @@ // //===----------------------------------------------------------------------===// +import AWSLambdaEvents import AWSLambdaRuntime import NIOCore @@ -29,6 +30,11 @@ struct SendNumbersWithPause: StreamingLambdaHandler { context: LambdaContext ) async throws { + // The payload here is a FunctionURLRequest. + // Check the body parameters for the business event + let payload = try JSONDecoder().decode(FunctionURLRequest.self, from: Data(event.readableBytesView)) + let _ = payload.body + // Send HTTP status code and headers before streaming the response body try await responseWriter.writeStatusAndHeaders( StreamingLambdaStatusAndHeadersResponse( From 07b0d7f99c71969b516992ee4ab32532c591ec81 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 30 Nov 2025 22:32:30 -0800 Subject: [PATCH 5/7] add missing dependencies --- Examples/Streaming+APIGateway/Package.swift | 3 ++- Examples/Streaming+FunctionUrl/Package.swift | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Examples/Streaming+APIGateway/Package.swift b/Examples/Streaming+APIGateway/Package.swift index d3f4006c..55b20eb1 100644 --- a/Examples/Streaming+APIGateway/Package.swift +++ b/Examples/Streaming+APIGateway/Package.swift @@ -20,7 +20,8 @@ let package = Package( .executableTarget( name: "StreamingNumbers", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), ], path: "Sources" ) diff --git a/Examples/Streaming+FunctionUrl/Package.swift b/Examples/Streaming+FunctionUrl/Package.swift index 06478d6b..bf3ceb9e 100644 --- a/Examples/Streaming+FunctionUrl/Package.swift +++ b/Examples/Streaming+FunctionUrl/Package.swift @@ -21,7 +21,8 @@ let package = Package( .executableTarget( name: "StreamingNumbers", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), ], path: "Sources" ) From 21b6ca71379a9d2173d8cfc81cd6b7d9921bd8a4 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 30 Nov 2025 22:32:40 -0800 Subject: [PATCH 6/7] swift format --- Examples/Streaming+APIGateway/Sources/main.swift | 4 ++-- Examples/Streaming+FunctionUrl/Sources/main.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Examples/Streaming+APIGateway/Sources/main.swift b/Examples/Streaming+APIGateway/Sources/main.swift index c78c6074..35356a07 100644 --- a/Examples/Streaming+APIGateway/Sources/main.swift +++ b/Examples/Streaming+APIGateway/Sources/main.swift @@ -30,11 +30,11 @@ struct SendNumbersWithPause: StreamingLambdaHandler { context: LambdaContext ) async throws { - // the payload here is an API Gateway V1 request + // the payload here is an API Gateway V1 request // Check the body of the request to extract the business event let payload = try JSONDecoder().decode(APIGatewayRequest.self, from: Data(event.readableBytesView)) let _ = payload.body - + // Send HTTP status code and headers before streaming the response body try await responseWriter.writeStatusAndHeaders( StreamingLambdaStatusAndHeadersResponse( diff --git a/Examples/Streaming+FunctionUrl/Sources/main.swift b/Examples/Streaming+FunctionUrl/Sources/main.swift index 49e4d63b..4f98d599 100644 --- a/Examples/Streaming+FunctionUrl/Sources/main.swift +++ b/Examples/Streaming+FunctionUrl/Sources/main.swift @@ -30,11 +30,11 @@ struct SendNumbersWithPause: StreamingLambdaHandler { context: LambdaContext ) async throws { - // The payload here is a FunctionURLRequest. + // The payload here is a FunctionURLRequest. // Check the body parameters for the business event let payload = try JSONDecoder().decode(FunctionURLRequest.self, from: Data(event.readableBytesView)) let _ = payload.body - + // Send HTTP status code and headers before streaming the response body try await responseWriter.writeStatusAndHeaders( StreamingLambdaStatusAndHeadersResponse( From ce61751d1717f60a2406785d00df3360f0297fd5 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 30 Nov 2025 22:51:08 -0800 Subject: [PATCH 7/7] add multi decoding strategy to the LambdaStreaming+Codable example --- .../Streaming+Codable/Sources/LambdaStreaming+Codable.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Examples/Streaming+Codable/Sources/LambdaStreaming+Codable.swift b/Examples/Streaming+Codable/Sources/LambdaStreaming+Codable.swift index 4cc8677f..31f5e9d9 100644 --- a/Examples/Streaming+Codable/Sources/LambdaStreaming+Codable.swift +++ b/Examples/Streaming+Codable/Sources/LambdaStreaming+Codable.swift @@ -90,11 +90,17 @@ public struct StreamingLambdaCodableAdapter< var decodedBody: Handler.Event! + // Try to decode the event. It first tries FunctionURLRequest, then APIGatewayRequest, then "as-is" + // try to decode the event as a FunctionURLRequest, then fetch its body attribute if let request = try? self.decoder.decode(FunctionURLRequest.self, from: event) { // decode the body as user-provided JSON type // this function handles the base64 decoding when needed decodedBody = try request.decodeBody(Handler.Event.self) + } else if let request = try? self.decoder.decode(APIGatewayRequest.self, from: event) { + // decode the body as user-provided JSON type + // this function handles the base64 decoding when needed + decodedBody = try request.decodeBody(Handler.Event.self) } else { // try to decode the event "as-is" with the provided JSON type decodedBody = try self.decoder.decode(Handler.Event.self, from: event)