Skip to content

Commit d8ee71f

Browse files
authored
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 #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 ```
1 parent d42ae69 commit d8ee71f

File tree

7 files changed

+128
-15
lines changed

7 files changed

+128
-15
lines changed

Sources/AWSLambdaRuntime/Lambda+LocalServer.swift

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import NIOHTTP1
2121
import NIOPosix
2222
import Synchronization
2323

24-
// This functionality is designed for local testing hence being a #if DEBUG flag.
24+
// This functionality is designed for local testing when the LocalServerSupport trait is enabled.
2525

2626
// For example:
2727
// try Lambda.withLocalServer {
@@ -42,18 +42,24 @@ extension Lambda {
4242
/// Execute code in the context of a mock Lambda server.
4343
///
4444
/// - parameters:
45+
/// - host: the hostname or IP address to listen on
46+
/// - port: the TCP port to listen to
4547
/// - invocationEndpoint: The endpoint to post events to.
4648
/// - body: Code to run within the context of the mock server. Typically this would be a Lambda.run function call.
4749
///
48-
/// - note: This API is designed strictly for local testing and is behind a DEBUG flag
50+
/// - note: This API is designed strictly for local testing when the LocalServerSupport trait is enabled.
4951
@usableFromInline
5052
static func withLocalServer(
53+
host: String,
54+
port: Int,
5155
invocationEndpoint: String? = nil,
5256
logger: Logger,
5357
_ body: sending @escaping () async throws -> Void
5458
) async throws {
5559
do {
5660
try await LambdaHTTPServer.withLocalServer(
61+
host: host,
62+
port: port,
5763
invocationEndpoint: invocationEndpoint,
5864
logger: logger
5965
) {
@@ -112,9 +118,9 @@ internal struct LambdaHTTPServer {
112118
}
113119

114120
static func withLocalServer<Result: Sendable>(
121+
host: String,
122+
port: Int,
115123
invocationEndpoint: String?,
116-
host: String = "127.0.0.1",
117-
port: Int = 7000,
118124
eventLoopGroup: MultiThreadedEventLoopGroup = .singleton,
119125
logger: Logger,
120126
_ closure: sending @escaping () async throws -> Result

Sources/AWSLambdaRuntime/LambdaRuntime.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,15 +124,23 @@ public final class LambdaRuntime<Handler>: Sendable where Handler: StreamingLamb
124124
} else {
125125

126126
#if LocalServerSupport
127+
127128
// we're not running on Lambda and we're compiled in DEBUG mode,
128129
// let's start a local server for testing
130+
131+
let host = Lambda.env("LOCAL_LAMBDA_HOST") ?? "127.0.0.1"
132+
let port = Lambda.env("LOCAL_LAMBDA_PORT").flatMap(Int.init) ?? 7000
133+
let endpoint = Lambda.env("LOCAL_LAMBDA_INVOCATION_ENDPOINT")
134+
129135
try await Lambda.withLocalServer(
130-
invocationEndpoint: Lambda.env("LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT"),
136+
host: host,
137+
port: port,
138+
invocationEndpoint: endpoint,
131139
logger: self.logger
132140
) {
133141

134142
try await LambdaRuntimeClient.withRuntimeClient(
135-
configuration: .init(ip: "127.0.0.1", port: 7000),
143+
configuration: .init(ip: host, port: port),
136144
eventLoop: self.eventLoop,
137145
logger: self.logger
138146
) { runtimeClient in
@@ -144,7 +152,7 @@ public final class LambdaRuntime<Handler>: Sendable where Handler: StreamingLamb
144152
}
145153
}
146154
#else
147-
// in release mode, we can't start a local server because the local server code is not compiled.
155+
// When the LocalServerSupport trait is disabled, we can't start a local server because the local server code is not compiled.
148156
throw LambdaRuntimeError(code: .missingLambdaRuntimeAPIEnvironmentVariable)
149157
#endif
150158
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Logging
16+
import NIOCore
17+
import NIOPosix
18+
import Testing
19+
20+
@testable import AWSLambdaRuntime
21+
22+
extension LambdaRuntimeTests {
23+
24+
@Test("Local server respects LOCAL_LAMBDA_PORT environment variable")
25+
func testLocalServerCustomPort() async throws {
26+
let customPort = 8080
27+
28+
// Set environment variable
29+
setenv("LOCAL_LAMBDA_PORT", "\(customPort)", 1)
30+
defer { unsetenv("LOCAL_LAMBDA_PORT") }
31+
32+
let result = try? await withThrowingTaskGroup(of: Bool.self) { group in
33+
34+
// start a local lambda + local server on custom port
35+
group.addTask {
36+
// Create a simple handler
37+
struct TestHandler: StreamingLambdaHandler {
38+
func handle(
39+
_ event: ByteBuffer,
40+
responseWriter: some LambdaResponseStreamWriter,
41+
context: LambdaContext
42+
) async throws {
43+
try await responseWriter.write(ByteBuffer(string: "test"))
44+
try await responseWriter.finish()
45+
}
46+
}
47+
48+
// create the Lambda Runtime
49+
let runtime = LambdaRuntime(
50+
handler: TestHandler(),
51+
logger: Logger(label: "test", factory: { _ in SwiftLogNoOpLogHandler() })
52+
)
53+
54+
// Start runtime
55+
try await runtime._run()
56+
57+
// we reach this line when the group is cancelled
58+
return false
59+
}
60+
61+
// start a client to check if something responds on the custom port
62+
group.addTask {
63+
// Give server time to start
64+
try await Task.sleep(for: .milliseconds(100))
65+
66+
// Verify server is listening on custom port
67+
return try await isPortResponding(host: "127.0.0.1", port: customPort)
68+
}
69+
70+
let first = try await group.next()
71+
group.cancelAll()
72+
return first ?? false
73+
74+
}
75+
76+
#expect(result == true)
77+
}
78+
79+
private func isPortResponding(host: String, port: Int) async throws -> Bool {
80+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
81+
82+
let bootstrap = ClientBootstrap(group: group)
83+
84+
do {
85+
let channel = try await bootstrap.connect(host: host, port: port).get()
86+
try await channel.close().get()
87+
try await group.shutdownGracefully()
88+
return true
89+
} catch {
90+
try await group.shutdownGracefully()
91+
return false
92+
}
93+
}
94+
}

Tests/AWSLambdaRuntimeTests/LambdaRuntime+ServiceLifeCycle.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ import ServiceLifecycle
1818
import Testing
1919
import Logging
2020

21-
@Suite
22-
struct LambdaRuntimeServiceLifecycleTests {
21+
extension LambdaRuntimeTests {
2322
@Test
2423
@available(LambdaSwift 2.0, *)
2524
func testLambdaRuntimeGracefulShutdown() async throws {

Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import Testing
2020

2121
@testable import AWSLambdaRuntime
2222

23-
@Suite("LambdaRuntimeTests")
23+
@Suite(.serialized)
2424
struct LambdaRuntimeTests {
2525

2626
@Test("LambdaRuntime can only be run once")

Tests/AWSLambdaRuntimeTests/MockLambdaServer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ final class MockLambdaServer<Behavior: LambdaServerBehavior> {
6060
init(
6161
behavior: Behavior,
6262
host: String = "127.0.0.1",
63-
port: Int = 7000,
63+
port: Int = 0,
6464
keepAlive: Bool = true,
6565
eventLoopGroup: MultiThreadedEventLoopGroup
6666
) {

readme.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -464,16 +464,22 @@ curl -v --header "Content-Type:\ application/json" --data @events/create-session
464464
* Connection #0 to host 127.0.0.1 left intact
465465
{"statusCode":200,"isBase64Encoded":false,"body":"...","headers":{"Access-Control-Allow-Origin":"*","Content-Type":"application\/json; charset=utf-8","Access-Control-Allow-Headers":"*"}}
466466
```
467-
### Modifying the local endpoint
467+
### Modifying the local server URI
468468

469-
By default, when using the local Lambda server, it listens on the `/invoke` endpoint.
469+
By default, when using the local Lambda server during your tests, it listens on `http://127.0.0.1:7000/invoke`.
470470

471-
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.
471+
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.
472+
473+
In these cases, you can use three environment variables to control the local server:
474+
475+
- Set `LOCAL_LAMBDA_HOST` to configure the local server to listen on a different TCP address.
476+
- Set `LOCAL_LAMBDA_PORT` to configure the local server to listen on a different TCP port.
477+
- Set `LOCAL_LAMBDA_INVOCATION_ENDPOINT` to force the local server to listen on a different endpoint.
472478

473479
Example:
474480

475481
```sh
476-
LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT=/2015-03-31/functions/function/invocations swift run
482+
LOCAL_LAMBDA_PORT=8080 LOCAL_LAMBDA_INVOCATION_ENDPOINT=/2015-03-31/functions/function/invocations swift run
477483
```
478484

479485
## Deploying your Swift Lambda functions

0 commit comments

Comments
 (0)