Files
swift-aws-lambda-runtime/Examples/Streaming+Codable/Sources/LambdaStreaming+Codable.swift
T
Sébastien Stormacq 3ddd64087d Add Streaming Lambda Examples with API Gateway and Function URL (#615)
## Overview

This PR reorganizes and enhances the streaming Lambda examples by
splitting them into two distinct examples that demonstrate different
invocation methods:

1. **Streaming+FunctionUrl** - Streaming responses via Lambda Function
URLs
2. **Streaming+APIGateway** - Streaming responses via API Gateway REST
API

## Changes

### 🔄 Restructured Examples

- **Renamed**: `Examples/Streaming/` → `Examples/Streaming+FunctionUrl/`
  - Maintains the original streaming example using Lambda Function URLs
  - Updated documentation to clarify Function URL-specific configuration
  - Improved AWS credentials handling in curl examples

- **New**: `Examples/Streaming+APIGateway/`
- Comprehensive example demonstrating API Gateway REST API with response
streaming
- Complete SAM template with proper IAM roles and streaming
configuration
  - Detailed documentation covering API Gateway-specific setup

### 📚 Documentation Improvements

#### Streaming+FunctionUrl
- Clarified that this example uses Lambda Function URLs
- Updated curl examples to use `eval $(aws configure export-credentials
--format env)` for cleaner credential handling
- Maintained all existing functionality and deployment instructions

#### Streaming+APIGateway (New)
- **316-line comprehensive README** covering:
  - Response streaming concepts and benefits
  - HTTP status code and header configuration
  - Streaming response body patterns
  - Local testing instructions
  - Complete SAM deployment guide with detailed template explanation
  - API Gateway-specific invocation with AWS Sigv4 authentication
  - Payload format documentation with example JSON
  - Security and reliability best practices
  - How API Gateway streaming works under the hood

### 🛠️ Technical Details

#### API Gateway Streaming Configuration
The new example demonstrates:
- Special Lambda URI: `/response-streaming-invocations` endpoint
- `responseTransferMode: STREAM` configuration
- IAM role with both `lambda:InvokeFunction` and
`lambda:InvokeWithResponseStream` permissions
- Proper timeout configuration (60s) to accommodate streaming duration

#### SAM Template Features
```yaml
- Lambda function with streaming support (arm64, provided.al2)
- API Gateway REST API with OpenAPI 3.0 definition
- IAM execution role for API Gateway to invoke Lambda with streaming
- Complete outputs for easy testing (API URL and Lambda ARN)
```

### 🔐 Security Enhancements

Both examples now include comprehensive security best practices:
- API Gateway access logging
- Throttling configuration
- AWS WAF integration recommendations
- Lambda concurrent execution limits
- Environment variable encryption
- Dead Letter Queue (DLQ) configuration
- VPC configuration guidance

### 🧪 Testing

Both examples support:
- **Local testing**: `swift run` with curl invocation on port 7000
- **AWS deployment**: Complete SAM templates with deployment
instructions
- **Authenticated invocation**: AWS Sigv4 examples with proper
credential handling

## Benefits

1. **Clearer separation**: Developers can now easily choose between
Function URLs and API Gateway based on their use case
2. **Better documentation**: Each example has tailored documentation for
its specific invocation method
3. **Production-ready**: Includes security best practices and proper IAM
configuration
4. **Easier testing**: Improved credential handling in curl examples

## Breaking Changes

None - this is purely additive. The original streaming example is
preserved as `Streaming+FunctionUrl`.

## Testing Checklist

- [x] Local testing works for both examples
- [x] SAM deployment templates are valid
- [x] Documentation is comprehensive and accurate
- [x] Security best practices are documented
- [x] Curl examples work with proper authentication

## Related Documentation

- [AWS Lambda Response
Streaming](https://docs.aws.amazon.com/lambda/latest/dg/configuration-response-streaming.html)
- [API Gateway Lambda Proxy Integration with
Streaming](https://docs.aws.amazon.com/apigateway/latest/developerguide/response-streaming-lambda-configure.html)
- [Lambda Function
URLs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html)
EOF

---------

Co-authored-by: Sebastien Stormacq <stormacq@amazon.lu>
2025-12-05 16:38:50 -08:00

193 lines
8.5 KiB
Swift

//===----------------------------------------------------------------------===//
//
// 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 AWSLambdaEvents
import AWSLambdaRuntime
import Logging
import NIOCore
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
/// A streaming handler protocol that receives a decoded JSON event and can stream responses.
/// This handler protocol supports response streaming and background work execution.
/// Background work can be executed after closing the response stream by calling
/// ``LambdaResponseStreamWriter/finish()`` or ``LambdaResponseStreamWriter/writeAndFinish(_:)``.
public protocol StreamingLambdaHandlerWithEvent: _Lambda_SendableMetatype {
/// Generic input type that will be decoded from JSON.
associatedtype Event: Decodable
/// The handler function that receives a decoded event and can stream responses.
/// - Parameters:
/// - event: The decoded event object.
/// - responseWriter: A ``LambdaResponseStreamWriter`` to write the invocation's response to.
/// If no response or error is written to `responseWriter` an error will be reported to the invoker.
/// - context: The ``LambdaContext`` containing the invocation's metadata.
/// - Throws:
/// How the thrown error will be handled by the runtime:
/// - An invocation error will be reported if the error is thrown before the first call to
/// ``LambdaResponseStreamWriter/write(_:)``.
/// - If the error is thrown after call(s) to ``LambdaResponseStreamWriter/write(_:)`` but before
/// a call to ``LambdaResponseStreamWriter/finish()``, the response stream will be closed and trailing
/// headers will be sent.
/// - If ``LambdaResponseStreamWriter/finish()`` has already been called before the error is thrown, the
/// error will be logged.
mutating func handle(
_ event: Event,
responseWriter: some LambdaResponseStreamWriter,
context: LambdaContext
) async throws
}
/// Adapts a ``StreamingLambdaHandlerWithEvent`` to work as a ``StreamingLambdaHandler``
/// by handling JSON decoding of the input event.
public struct StreamingLambdaCodableAdapter<
Handler: StreamingLambdaHandlerWithEvent,
Decoder: LambdaEventDecoder
>: StreamingLambdaHandler where Handler.Event: Decodable {
@usableFromInline var handler: Handler
@usableFromInline let decoder: Decoder
/// Initialize with a custom decoder and handler.
/// - Parameters:
/// - decoder: The decoder to use for parsing the input event.
/// - handler: The streaming handler that works with decoded events.
@inlinable
public init(decoder: sending Decoder, handler: sending Handler) {
self.decoder = decoder
self.handler = handler
}
/// Handles the raw ByteBuffer by decoding it and passing to the underlying handler.
/// This function attempts to decode the event as a `FunctionURLRequest` first, which allows for
/// handling Function URL requests that may have a base64-encoded body.
/// If the decoding fails, it falls back to decoding the event "as-is" with the provided JSON type.
/// - Parameters:
/// - event: The raw ByteBuffer event to decode.
/// - responseWriter: The response writer to pass to the underlying handler.
/// - context: The Lambda context.
@inlinable
public mutating func handle(
_ event: ByteBuffer,
responseWriter: some LambdaResponseStreamWriter,
context: LambdaContext
) async throws {
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)
}
// and pass it to the handler
try await self.handler.handle(decodedBody, responseWriter: responseWriter, context: context)
}
}
/// A closure-based streaming handler that works with decoded JSON events.
/// Allows for a streaming handler to be defined in a clean manner, leveraging Swift's trailing closure syntax.
public struct StreamingFromEventClosureHandler<Event: Decodable>: StreamingLambdaHandlerWithEvent {
let body: @Sendable (Event, LambdaResponseStreamWriter, LambdaContext) async throws -> Void
/// Initialize with a closure that receives a decoded event.
/// - Parameter body: The handler closure that receives a decoded event, response writer, and context.
public init(
body: @Sendable @escaping (Event, LambdaResponseStreamWriter, LambdaContext) async throws -> Void
) {
self.body = body
}
/// Calls the provided closure with the decoded event.
/// - Parameters:
/// - event: The decoded event object.
/// - responseWriter: The response writer for streaming output.
/// - context: The Lambda context.
public func handle(
_ event: Event,
responseWriter: some LambdaResponseStreamWriter,
context: LambdaContext
) async throws {
try await self.body(event, responseWriter, context)
}
}
extension StreamingLambdaCodableAdapter {
/// Initialize with a JSON decoder and handler.
/// - Parameters:
/// - decoder: The JSON decoder to use. Defaults to `JSONDecoder()`.
/// - handler: The streaming handler that works with decoded events.
public init(
decoder: JSONDecoder = JSONDecoder(),
handler: sending Handler
) where Decoder == LambdaJSONEventDecoder {
self.init(decoder: LambdaJSONEventDecoder(decoder), handler: handler)
}
}
extension LambdaRuntime {
/// Initialize with a streaming handler that receives decoded JSON events.
/// - Parameters:
/// - decoder: The JSON decoder to use. Defaults to `JSONDecoder()`.
/// - logger: The logger to use. Defaults to a logger with label "LambdaRuntime".
/// - streamingBody: The handler closure that receives a decoded event.
public convenience init<Event: Decodable>(
decoder: JSONDecoder = JSONDecoder(),
logger: Logger = Logger(label: "LambdaRuntime"),
streamingBody: @Sendable @escaping (Event, LambdaResponseStreamWriter, LambdaContext) async throws -> Void
)
where
Handler == StreamingLambdaCodableAdapter<
StreamingFromEventClosureHandler<Event>,
LambdaJSONEventDecoder
>
{
let closureHandler = StreamingFromEventClosureHandler(body: streamingBody)
let adapter = StreamingLambdaCodableAdapter(
decoder: decoder,
handler: closureHandler
)
self.init(handler: adapter, logger: logger)
}
/// Initialize with a custom streaming handler that receives decoded events.
/// - Parameters:
/// - decoder: The decoder to use for parsing input events.
/// - handler: The streaming handler.
/// - logger: The logger to use.
public convenience init<StreamingHandler: StreamingLambdaHandlerWithEvent, Decoder: LambdaEventDecoder>(
decoder: sending Decoder,
handler: sending StreamingHandler,
logger: Logger = Logger(label: "LambdaRuntime")
) where Handler == StreamingLambdaCodableAdapter<StreamingHandler, Decoder> {
let adapter = StreamingLambdaCodableAdapter(decoder: decoder, handler: handler)
self.init(handler: adapter, logger: logger)
}
}