add support for LOCAL_LAMBDA_PORT / HOST(#557)

Allows users to define on which port the Local server listens to, using
the `LOCAL_LAMBDA_PORT` environment variable.

While being at it, I also added `LOCAL_LAMBDA_HOST` if the user wants to
bind on a specific IP address.

I renamed `LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT` to
`LOCAL_LAMBDA_INVOCATION_ENDPOINT` for consistency.

### Motivation:

Addresses
https://github.com/swift-server/swift-aws-lambda-runtime/issues/556

### Modifications:

- When run outside of the Lambda execution environment, check for the
value of `LOCAL_LAMBDA_PORT` and passes it down to the Lambda HTTP Local
Server and runtime client.

- Add a unit test 

### Result:

```
LAMBDA_USE_LOCAL_DEPS=../.. LOCAL_LAMBDA_PORT=8888 swift run                  

2025-09-01T21:55:22+0200 info LambdaRuntime: host="127.0.0.1" port=8888 [AWSLambdaRuntime] Server started and listening
```
This commit is contained in:
Sébastien Stormacq
2025-09-03 18:22:57 +02:00
committed by GitHub
parent d42ae6975e
commit d8ee71fc09
7 changed files with 128 additions and 15 deletions
@@ -21,7 +21,7 @@ import NIOHTTP1
import NIOPosix
import Synchronization
// This functionality is designed for local testing hence being a #if DEBUG flag.
// This functionality is designed for local testing when the LocalServerSupport trait is enabled.
// For example:
// try Lambda.withLocalServer {
@@ -42,18 +42,24 @@ extension Lambda {
/// Execute code in the context of a mock Lambda server.
///
/// - parameters:
/// - host: the hostname or IP address to listen on
/// - port: the TCP port to listen to
/// - invocationEndpoint: The endpoint to post events to.
/// - body: Code to run within the context of the mock server. Typically this would be a Lambda.run function call.
///
/// - note: This API is designed strictly for local testing and is behind a DEBUG flag
/// - note: This API is designed strictly for local testing when the LocalServerSupport trait is enabled.
@usableFromInline
static func withLocalServer(
host: String,
port: Int,
invocationEndpoint: String? = nil,
logger: Logger,
_ body: sending @escaping () async throws -> Void
) async throws {
do {
try await LambdaHTTPServer.withLocalServer(
host: host,
port: port,
invocationEndpoint: invocationEndpoint,
logger: logger
) {
@@ -112,9 +118,9 @@ internal struct LambdaHTTPServer {
}
static func withLocalServer<Result: Sendable>(
host: String,
port: Int,
invocationEndpoint: String?,
host: String = "127.0.0.1",
port: Int = 7000,
eventLoopGroup: MultiThreadedEventLoopGroup = .singleton,
logger: Logger,
_ closure: sending @escaping () async throws -> Result
+11 -3
View File
@@ -124,15 +124,23 @@ public final class LambdaRuntime<Handler>: Sendable where Handler: StreamingLamb
} else {
#if LocalServerSupport
// we're not running on Lambda and we're compiled in DEBUG mode,
// let's start a local server for testing
let host = Lambda.env("LOCAL_LAMBDA_HOST") ?? "127.0.0.1"
let port = Lambda.env("LOCAL_LAMBDA_PORT").flatMap(Int.init) ?? 7000
let endpoint = Lambda.env("LOCAL_LAMBDA_INVOCATION_ENDPOINT")
try await Lambda.withLocalServer(
invocationEndpoint: Lambda.env("LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT"),
host: host,
port: port,
invocationEndpoint: endpoint,
logger: self.logger
) {
try await LambdaRuntimeClient.withRuntimeClient(
configuration: .init(ip: "127.0.0.1", port: 7000),
configuration: .init(ip: host, port: port),
eventLoop: self.eventLoop,
logger: self.logger
) { runtimeClient in
@@ -144,7 +152,7 @@ public final class LambdaRuntime<Handler>: Sendable where Handler: StreamingLamb
}
}
#else
// in release mode, we can't start a local server because the local server code is not compiled.
// When the LocalServerSupport trait is disabled, we can't start a local server because the local server code is not compiled.
throw LambdaRuntimeError(code: .missingLambdaRuntimeAPIEnvironmentVariable)
#endif
}
@@ -0,0 +1,94 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAWSLambdaRuntime open source project
//
// Copyright (c) 2025 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 Logging
import NIOCore
import NIOPosix
import Testing
@testable import AWSLambdaRuntime
extension LambdaRuntimeTests {
@Test("Local server respects LOCAL_LAMBDA_PORT environment variable")
func testLocalServerCustomPort() async throws {
let customPort = 8080
// Set environment variable
setenv("LOCAL_LAMBDA_PORT", "\(customPort)", 1)
defer { unsetenv("LOCAL_LAMBDA_PORT") }
let result = try? await withThrowingTaskGroup(of: Bool.self) { group in
// start a local lambda + local server on custom port
group.addTask {
// Create a simple handler
struct TestHandler: StreamingLambdaHandler {
func handle(
_ event: ByteBuffer,
responseWriter: some LambdaResponseStreamWriter,
context: LambdaContext
) async throws {
try await responseWriter.write(ByteBuffer(string: "test"))
try await responseWriter.finish()
}
}
// create the Lambda Runtime
let runtime = LambdaRuntime(
handler: TestHandler(),
logger: Logger(label: "test", factory: { _ in SwiftLogNoOpLogHandler() })
)
// Start runtime
try await runtime._run()
// we reach this line when the group is cancelled
return false
}
// start a client to check if something responds on the custom port
group.addTask {
// Give server time to start
try await Task.sleep(for: .milliseconds(100))
// Verify server is listening on custom port
return try await isPortResponding(host: "127.0.0.1", port: customPort)
}
let first = try await group.next()
group.cancelAll()
return first ?? false
}
#expect(result == true)
}
private func isPortResponding(host: String, port: Int) async throws -> Bool {
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let bootstrap = ClientBootstrap(group: group)
do {
let channel = try await bootstrap.connect(host: host, port: port).get()
try await channel.close().get()
try await group.shutdownGracefully()
return true
} catch {
try await group.shutdownGracefully()
return false
}
}
}
@@ -18,8 +18,7 @@ import ServiceLifecycle
import Testing
import Logging
@Suite
struct LambdaRuntimeServiceLifecycleTests {
extension LambdaRuntimeTests {
@Test
@available(LambdaSwift 2.0, *)
func testLambdaRuntimeGracefulShutdown() async throws {
@@ -20,7 +20,7 @@ import Testing
@testable import AWSLambdaRuntime
@Suite("LambdaRuntimeTests")
@Suite(.serialized)
struct LambdaRuntimeTests {
@Test("LambdaRuntime can only be run once")
@@ -60,7 +60,7 @@ final class MockLambdaServer<Behavior: LambdaServerBehavior> {
init(
behavior: Behavior,
host: String = "127.0.0.1",
port: Int = 7000,
port: Int = 0,
keepAlive: Bool = true,
eventLoopGroup: MultiThreadedEventLoopGroup
) {
+10 -4
View File
@@ -464,16 +464,22 @@ curl -v --header "Content-Type:\ application/json" --data @events/create-session
* Connection #0 to host 127.0.0.1 left intact
{"statusCode":200,"isBase64Encoded":false,"body":"...","headers":{"Access-Control-Allow-Origin":"*","Content-Type":"application\/json; charset=utf-8","Access-Control-Allow-Headers":"*"}}
```
### Modifying the local endpoint
### Modifying the local server URI
By default, when using the local Lambda server, it listens on the `/invoke` endpoint.
By default, when using the local Lambda server during your tests, it listens on `http://127.0.0.1:7000/invoke`.
Some testing tools, such as the [AWS Lambda runtime interface emulator](https://docs.aws.amazon.com/lambda/latest/dg/images-test.html), require a different endpoint. In that case, you can use the `LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT` environment variable to force the runtime to listen on a different endpoint.
Some testing tools, such as the [AWS Lambda runtime interface emulator](https://docs.aws.amazon.com/lambda/latest/dg/images-test.html), require a different endpoint, the port might be used, or you may want to bind a specific IP address.
In these cases, you can use three environment variables to control the local server:
- Set `LOCAL_LAMBDA_HOST` to configure the local server to listen on a different TCP address.
- Set `LOCAL_LAMBDA_PORT` to configure the local server to listen on a different TCP port.
- Set `LOCAL_LAMBDA_INVOCATION_ENDPOINT` to force the local server to listen on a different endpoint.
Example:
```sh
LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT=/2015-03-31/functions/function/invocations swift run
LOCAL_LAMBDA_PORT=8080 LOCAL_LAMBDA_INVOCATION_ENDPOINT=/2015-03-31/functions/function/invocations swift run
```
## Deploying your Swift Lambda functions