diff --git a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift index b85affd6..9eed4389 100644 --- a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift @@ -14,306 +14,308 @@ // commented out as long as we have a fix for Swift 6 language mode CI -// #if DEBUG -// import Dispatch -// import Logging -// import NIOConcurrencyHelpers -// import NIOCore -// import NIOHTTP1 -// import NIOPosix - -// // This functionality is designed for local testing hence beind a #if DEBUG flag. -// // For example: -// // -// // try Lambda.withLocalServer { -// // Lambda.run { (context: LambdaContext, event: String, callback: @escaping (Result) -> Void) in -// // callback(.success("Hello, \(event)!")) -// // } -// // } -// extension Lambda { -// /// Execute code in the context of a mock Lambda server. -// /// -// /// - parameters: -// /// - 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 -// static func withLocalServer(invocationEndpoint: String? = nil, _ body: @escaping () -> Value) throws -> Value -// { -// let server = LocalLambda.Server(invocationEndpoint: invocationEndpoint) -// try server.start().wait() -// defer { try! server.stop() } -// return body() -// } -// } - -// // MARK: - Local Mock Server - -// private enum LocalLambda { -// struct Server { -// private let logger: Logger -// private let group: EventLoopGroup -// private let host: String -// private let port: Int -// private let invocationEndpoint: String - -// init(invocationEndpoint: String?) { -// var logger = Logger(label: "LocalLambdaServer") -// logger.logLevel = .info -// self.logger = logger -// self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) -// self.host = "127.0.0.1" -// self.port = 0 -// self.invocationEndpoint = invocationEndpoint ?? "/invoke" -// } - -// func start() -> EventLoopFuture { -// let bootstrap = ServerBootstrap(group: group) -// .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) -// .childChannelInitializer { channel in -// channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap { _ in -// channel.pipeline.addHandler( -// HTTPHandler(logger: self.logger, invocationEndpoint: self.invocationEndpoint) -// ) -// } -// } -// return bootstrap.bind(host: self.host, port: self.port).flatMap { channel -> EventLoopFuture in -// guard channel.localAddress != nil else { -// return channel.eventLoop.makeFailedFuture(ServerError.cantBind) -// } -// self.logger.info( -// "LocalLambdaServer started and listening on \(self.host):\(self.port), receiving events on \(self.invocationEndpoint)" -// ) -// return channel.eventLoop.makeSucceededFuture(()) -// } -// } - -// func stop() throws { -// try self.group.syncShutdownGracefully() -// } -// } - -// final class HTTPHandler: ChannelInboundHandler { -// public typealias InboundIn = HTTPServerRequestPart -// public typealias OutboundOut = HTTPServerResponsePart - -// private var pending = CircularBuffer<(head: HTTPRequestHead, body: ByteBuffer?)>() - -// private static var invocations = CircularBuffer() -// private static var invocationState = InvocationState.waitingForLambdaRequest - -// private let logger: Logger -// private let invocationEndpoint: String - -// init(logger: Logger, invocationEndpoint: String) { -// self.logger = logger -// self.invocationEndpoint = invocationEndpoint -// } - -// func channelRead(context: ChannelHandlerContext, data: NIOAny) { -// let requestPart = unwrapInboundIn(data) - -// switch requestPart { -// case .head(let head): -// self.pending.append((head: head, body: nil)) -// case .body(var buffer): -// var request = self.pending.removeFirst() -// if request.body == nil { -// request.body = buffer -// } else { -// request.body!.writeBuffer(&buffer) -// } -// self.pending.prepend(request) -// case .end: -// let request = self.pending.removeFirst() -// self.processRequest(context: context, request: request) -// } -// } - -// func processRequest(context: ChannelHandlerContext, request: (head: HTTPRequestHead, body: ByteBuffer?)) { -// switch (request.head.method, request.head.uri) { -// // this endpoint is called by the client invoking the lambda -// case (.POST, let url) where url.hasSuffix(self.invocationEndpoint): -// guard let work = request.body else { -// return self.writeResponse(context: context, response: .init(status: .badRequest)) -// } -// let requestID = "\(DispatchTime.now().uptimeNanoseconds)" // FIXME: -// let promise = context.eventLoop.makePromise(of: Response.self) -// promise.futureResult.whenComplete { result in -// switch result { -// case .failure(let error): -// self.logger.error("invocation error: \(error)") -// self.writeResponse(context: context, response: .init(status: .internalServerError)) -// case .success(let response): -// self.writeResponse(context: context, response: response) -// } -// } -// let invocation = Invocation(requestID: requestID, request: work, responsePromise: promise) -// switch Self.invocationState { -// case .waitingForInvocation(let promise): -// promise.succeed(invocation) -// case .waitingForLambdaRequest, .waitingForLambdaResponse: -// Self.invocations.append(invocation) -// } - -// // lambda invocation using the wrong http method -// case (_, let url) where url.hasSuffix(self.invocationEndpoint): -// self.writeResponse(context: context, status: .methodNotAllowed) - -// // /next endpoint is called by the lambda polling for work -// case (.GET, let url) where url.hasSuffix(Consts.getNextInvocationURLSuffix): -// // check if our server is in the correct state -// guard case .waitingForLambdaRequest = Self.invocationState else { -// self.logger.error("invalid invocation state \(Self.invocationState)") -// self.writeResponse(context: context, response: .init(status: .unprocessableEntity)) -// return -// } - -// // pop the first task from the queue -// switch Self.invocations.popFirst() { -// case .none: -// // if there is nothing in the queue, -// // create a promise that we can fullfill when we get a new task -// let promise = context.eventLoop.makePromise(of: Invocation.self) -// promise.futureResult.whenComplete { result in -// switch result { -// case .failure(let error): -// self.logger.error("invocation error: \(error)") -// self.writeResponse(context: context, status: .internalServerError) -// case .success(let invocation): -// Self.invocationState = .waitingForLambdaResponse(invocation) -// self.writeResponse(context: context, response: invocation.makeResponse()) -// } -// } -// Self.invocationState = .waitingForInvocation(promise) -// case .some(let invocation): -// // if there is a task pending, we can immediatly respond with it. -// Self.invocationState = .waitingForLambdaResponse(invocation) -// self.writeResponse(context: context, response: invocation.makeResponse()) -// } - -// // :requestID/response endpoint is called by the lambda posting the response -// case (.POST, let url) where url.hasSuffix(Consts.postResponseURLSuffix): -// let parts = request.head.uri.split(separator: "/") -// guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { -// // the request is malformed, since we were expecting a requestId in the path -// return self.writeResponse(context: context, status: .badRequest) -// } -// guard case .waitingForLambdaResponse(let invocation) = Self.invocationState else { -// // a response was send, but we did not expect to receive one -// self.logger.error("invalid invocation state \(Self.invocationState)") -// return self.writeResponse(context: context, status: .unprocessableEntity) -// } -// guard requestID == invocation.requestID else { -// // the request's requestId is not matching the one we are expecting -// self.logger.error( -// "invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)" -// ) -// return self.writeResponse(context: context, status: .badRequest) -// } - -// invocation.responsePromise.succeed(.init(status: .ok, body: request.body)) -// self.writeResponse(context: context, status: .accepted) -// Self.invocationState = .waitingForLambdaRequest - -// // :requestID/error endpoint is called by the lambda posting an error response -// case (.POST, let url) where url.hasSuffix(Consts.postErrorURLSuffix): -// let parts = request.head.uri.split(separator: "/") -// guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { -// // the request is malformed, since we were expecting a requestId in the path -// return self.writeResponse(context: context, status: .badRequest) -// } -// guard case .waitingForLambdaResponse(let invocation) = Self.invocationState else { -// // a response was send, but we did not expect to receive one -// self.logger.error("invalid invocation state \(Self.invocationState)") -// return self.writeResponse(context: context, status: .unprocessableEntity) -// } -// guard requestID == invocation.requestID else { -// // the request's requestId is not matching the one we are expecting -// self.logger.error( -// "invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)" -// ) -// return self.writeResponse(context: context, status: .badRequest) -// } - -// invocation.responsePromise.succeed(.init(status: .internalServerError, body: request.body)) -// self.writeResponse(context: context, status: .accepted) -// Self.invocationState = .waitingForLambdaRequest - -// // unknown call -// default: -// self.writeResponse(context: context, status: .notFound) -// } -// } - -// func writeResponse(context: ChannelHandlerContext, status: HTTPResponseStatus) { -// self.writeResponse(context: context, response: .init(status: status)) -// } - -// func writeResponse(context: ChannelHandlerContext, response: Response) { -// var headers = HTTPHeaders(response.headers ?? []) -// headers.add(name: "content-length", value: "\(response.body?.readableBytes ?? 0)") -// let head = HTTPResponseHead( -// version: HTTPVersion(major: 1, minor: 1), -// status: response.status, -// headers: headers -// ) - -// context.write(wrapOutboundOut(.head(head))).whenFailure { error in -// self.logger.error("\(self) write error \(error)") -// } - -// if let buffer = response.body { -// context.write(wrapOutboundOut(.body(.byteBuffer(buffer)))).whenFailure { error in -// self.logger.error("\(self) write error \(error)") -// } -// } - -// context.writeAndFlush(wrapOutboundOut(.end(nil))).whenComplete { result in -// if case .failure(let error) = result { -// self.logger.error("\(self) write error \(error)") -// } -// } -// } - -// struct Response { -// var status: HTTPResponseStatus = .ok -// var headers: [(String, String)]? -// var body: ByteBuffer? -// } - -// struct Invocation { -// let requestID: String -// let request: ByteBuffer -// let responsePromise: EventLoopPromise - -// func makeResponse() -> Response { -// var response = Response() -// response.body = self.request -// // required headers -// response.headers = [ -// (AmazonHeaders.requestID, self.requestID), -// ( -// AmazonHeaders.invokedFunctionARN, -// "arn:aws:lambda:us-east-1:\(Int16.random(in: Int16.min ... Int16.max)):function:custom-runtime" -// ), -// (AmazonHeaders.traceID, "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1"), -// (AmazonHeaders.deadline, "\(DispatchWallTime.distantFuture.millisSinceEpoch)"), -// ] -// return response -// } -// } - -// enum InvocationState { -// case waitingForInvocation(EventLoopPromise) -// case waitingForLambdaRequest -// case waitingForLambdaResponse(Invocation) -// } -// } - -// enum ServerError: Error { -// case notReady -// case cantBind +#if DEBUG +import Dispatch +import Logging +import NIOConcurrencyHelpers +import NIOCore +import NIOHTTP1 +import NIOPosix + +// This functionality is designed for local testing hence being a #if DEBUG flag. +// For example: +// +// try Lambda.withLocalServer { +// Lambda.run { (context: LambdaContext, event: String, callback: @escaping (Result) -> Void) in +// callback(.success("Hello, \(event)!")) // } // } -// #endif +extension Lambda { + /// Execute code in the context of a mock Lambda server. + /// + /// - parameters: + /// - 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 + static func withLocalServer( + invocationEndpoint: String? = nil, + _ body: @escaping () async throws -> Value + ) async throws -> Value { + let server = LocalLambda.Server(invocationEndpoint: invocationEndpoint) + try await server.start().get() + defer { try! server.stop() } + return try await body() + } +} + +// MARK: - Local Mock Server + +private enum LocalLambda { + struct Server { + private let logger: Logger + private let group: EventLoopGroup + private let host: String + private let port: Int + private let invocationEndpoint: String + + init(invocationEndpoint: String?) { + var logger = Logger(label: "LocalLambdaServer") + logger.logLevel = .info + self.logger = logger + self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + self.host = "127.0.0.1" + self.port = 7000 + self.invocationEndpoint = invocationEndpoint ?? "/invoke" + } + + func start() -> EventLoopFuture { + let bootstrap = ServerBootstrap(group: group) + .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) + .childChannelInitializer { channel in + channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap { _ in + channel.pipeline.addHandler( + HTTPHandler(logger: self.logger, invocationEndpoint: self.invocationEndpoint) + ) + } + } + return bootstrap.bind(host: self.host, port: self.port).flatMap { channel -> EventLoopFuture in + guard channel.localAddress != nil else { + return channel.eventLoop.makeFailedFuture(ServerError.cantBind) + } + self.logger.info( + "LocalLambdaServer started and listening on \(self.host):\(self.port), receiving events on \(self.invocationEndpoint)" + ) + return channel.eventLoop.makeSucceededFuture(()) + } + } + + func stop() throws { + try self.group.syncShutdownGracefully() + } + } + + final class HTTPHandler: ChannelInboundHandler { + public typealias InboundIn = HTTPServerRequestPart + public typealias OutboundOut = HTTPServerResponsePart + + private var pending = CircularBuffer<(head: HTTPRequestHead, body: ByteBuffer?)>() + + private static var invocations = CircularBuffer() + private static var invocationState = InvocationState.waitingForLambdaRequest + + private let logger: Logger + private let invocationEndpoint: String + + init(logger: Logger, invocationEndpoint: String) { + self.logger = logger + self.invocationEndpoint = invocationEndpoint + } + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let requestPart = unwrapInboundIn(data) + + switch requestPart { + case .head(let head): + self.pending.append((head: head, body: nil)) + case .body(var buffer): + var request = self.pending.removeFirst() + if request.body == nil { + request.body = buffer + } else { + request.body!.writeBuffer(&buffer) + } + self.pending.prepend(request) + case .end: + let request = self.pending.removeFirst() + self.processRequest(context: context, request: request) + } + } + + func processRequest(context: ChannelHandlerContext, request: (head: HTTPRequestHead, body: ByteBuffer?)) { + switch (request.head.method, request.head.uri) { + // this endpoint is called by the client invoking the lambda + case (.POST, let url) where url.hasSuffix(self.invocationEndpoint): + guard let work = request.body else { + return self.writeResponse(context: context, response: .init(status: .badRequest)) + } + let requestID = "\(DispatchTime.now().uptimeNanoseconds)" // FIXME: + let promise = context.eventLoop.makePromise(of: Response.self) + promise.futureResult.whenComplete { result in + switch result { + case .failure(let error): + self.logger.error("invocation error: \(error)") + self.writeResponse(context: context, response: .init(status: .internalServerError)) + case .success(let response): + self.writeResponse(context: context, response: response) + } + } + let invocation = Invocation(requestID: requestID, request: work, responsePromise: promise) + switch Self.invocationState { + case .waitingForInvocation(let promise): + promise.succeed(invocation) + case .waitingForLambdaRequest, .waitingForLambdaResponse: + Self.invocations.append(invocation) + } + + // lambda invocation using the wrong http method + case (_, let url) where url.hasSuffix(self.invocationEndpoint): + self.writeResponse(context: context, status: .methodNotAllowed) + + // /next endpoint is called by the lambda polling for work + case (.GET, let url) where url.hasSuffix(Consts.getNextInvocationURLSuffix): + // check if our server is in the correct state + guard case .waitingForLambdaRequest = Self.invocationState else { + self.logger.error("invalid invocation state \(Self.invocationState)") + self.writeResponse(context: context, response: .init(status: .unprocessableEntity)) + return + } + + // pop the first task from the queue + switch Self.invocations.popFirst() { + case .none: + // if there is nothing in the queue, + // create a promise that we can fullfill when we get a new task + let promise = context.eventLoop.makePromise(of: Invocation.self) + promise.futureResult.whenComplete { result in + switch result { + case .failure(let error): + self.logger.error("invocation error: \(error)") + self.writeResponse(context: context, status: .internalServerError) + case .success(let invocation): + Self.invocationState = .waitingForLambdaResponse(invocation) + self.writeResponse(context: context, response: invocation.makeResponse()) + } + } + Self.invocationState = .waitingForInvocation(promise) + case .some(let invocation): + // if there is a task pending, we can immediately respond with it. + Self.invocationState = .waitingForLambdaResponse(invocation) + self.writeResponse(context: context, response: invocation.makeResponse()) + } + + // :requestID/response endpoint is called by the lambda posting the response + case (.POST, let url) where url.hasSuffix(Consts.postResponseURLSuffix): + let parts = request.head.uri.split(separator: "/") + guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { + // the request is malformed, since we were expecting a requestId in the path + return self.writeResponse(context: context, status: .badRequest) + } + guard case .waitingForLambdaResponse(let invocation) = Self.invocationState else { + // a response was send, but we did not expect to receive one + self.logger.error("invalid invocation state \(Self.invocationState)") + return self.writeResponse(context: context, status: .unprocessableEntity) + } + guard requestID == invocation.requestID else { + // the request's requestId is not matching the one we are expecting + self.logger.error( + "invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)" + ) + return self.writeResponse(context: context, status: .badRequest) + } + + invocation.responsePromise.succeed(.init(status: .ok, body: request.body)) + self.writeResponse(context: context, status: .accepted) + Self.invocationState = .waitingForLambdaRequest + + // :requestID/error endpoint is called by the lambda posting an error response + case (.POST, let url) where url.hasSuffix(Consts.postErrorURLSuffix): + let parts = request.head.uri.split(separator: "/") + guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { + // the request is malformed, since we were expecting a requestId in the path + return self.writeResponse(context: context, status: .badRequest) + } + guard case .waitingForLambdaResponse(let invocation) = Self.invocationState else { + // a response was send, but we did not expect to receive one + self.logger.error("invalid invocation state \(Self.invocationState)") + return self.writeResponse(context: context, status: .unprocessableEntity) + } + guard requestID == invocation.requestID else { + // the request's requestId is not matching the one we are expecting + self.logger.error( + "invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)" + ) + return self.writeResponse(context: context, status: .badRequest) + } + + invocation.responsePromise.succeed(.init(status: .internalServerError, body: request.body)) + self.writeResponse(context: context, status: .accepted) + Self.invocationState = .waitingForLambdaRequest + + // unknown call + default: + self.writeResponse(context: context, status: .notFound) + } + } + + func writeResponse(context: ChannelHandlerContext, status: HTTPResponseStatus) { + self.writeResponse(context: context, response: .init(status: status)) + } + + func writeResponse(context: ChannelHandlerContext, response: Response) { + var headers = HTTPHeaders(response.headers ?? []) + headers.add(name: "content-length", value: "\(response.body?.readableBytes ?? 0)") + let head = HTTPResponseHead( + version: HTTPVersion(major: 1, minor: 1), + status: response.status, + headers: headers + ) + + context.write(wrapOutboundOut(.head(head))).whenFailure { error in + self.logger.error("\(self) write error \(error)") + } + + if let buffer = response.body { + context.write(wrapOutboundOut(.body(.byteBuffer(buffer)))).whenFailure { error in + self.logger.error("\(self) write error \(error)") + } + } + + context.writeAndFlush(wrapOutboundOut(.end(nil))).whenComplete { result in + if case .failure(let error) = result { + self.logger.error("\(self) write error \(error)") + } + } + } + + struct Response { + var status: HTTPResponseStatus = .ok + var headers: [(String, String)]? + var body: ByteBuffer? + } + + struct Invocation { + let requestID: String + let request: ByteBuffer + let responsePromise: EventLoopPromise + + func makeResponse() -> Response { + var response = Response() + response.body = self.request + // required headers + response.headers = [ + (AmazonHeaders.requestID, self.requestID), + ( + AmazonHeaders.invokedFunctionARN, + "arn:aws:lambda:us-east-1:\(Int16.random(in: Int16.min ... Int16.max)):function:custom-runtime" + ), + (AmazonHeaders.traceID, "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1"), + (AmazonHeaders.deadline, "\(DispatchWallTime.distantFuture.millisSinceEpoch)"), + ] + return response + } + } + + enum InvocationState { + case waitingForInvocation(EventLoopPromise) + case waitingForLambdaRequest + case waitingForLambdaResponse(Invocation) + } + } + + enum ServerError: Error { + case notReady + case cantBind + } +} +#endif diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift b/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift index 726c39a4..86274836 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift @@ -43,14 +43,6 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St } public func run() async throws { - guard let runtimeEndpoint = Lambda.env("AWS_LAMBDA_RUNTIME_API") else { - throw LambdaRuntimeError(code: .missingLambdaRuntimeAPIEnvironmentVariable) - } - - let ipAndPort = runtimeEndpoint.split(separator: ":", maxSplits: 1) - let ip = String(ipAndPort[0]) - guard let port = Int(ipAndPort[1]) else { throw LambdaRuntimeError(code: .invalidPort) } - let handler = self.handlerMutex.withLockedValue { handler in let result = handler handler = nil @@ -61,16 +53,45 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St throw LambdaRuntimeError(code: .runtimeCanOnlyBeStartedOnce) } - try await LambdaRuntimeClient.withRuntimeClient( - configuration: .init(ip: ip, port: port), - eventLoop: self.eventLoop, - logger: self.logger - ) { runtimeClient in - try await Lambda.runLoop( - runtimeClient: runtimeClient, - handler: handler, + // are we running inside an AWS Lambda runtime environment ? + // AWS_LAMBDA_RUNTIME_API is set when running on Lambda + // https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html + if let runtimeEndpoint = Lambda.env("AWS_LAMBDA_RUNTIME_API") { + + let ipAndPort = runtimeEndpoint.split(separator: ":", maxSplits: 1) + let ip = String(ipAndPort[0]) + guard let port = Int(ipAndPort[1]) else { throw LambdaRuntimeError(code: .invalidPort) } + + try await LambdaRuntimeClient.withRuntimeClient( + configuration: .init(ip: ip, port: port), + eventLoop: self.eventLoop, logger: self.logger - ) + ) { runtimeClient in + try await Lambda.runLoop( + runtimeClient: runtimeClient, + handler: handler, + logger: self.logger + ) + } + + } else { + + // we're not running on Lambda, let's start a local server for testing + try await Lambda.withLocalServer(invocationEndpoint: Lambda.env("LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT")) + { + + try await LambdaRuntimeClient.withRuntimeClient( + configuration: .init(ip: "127.0.0.1", port: 7000), + eventLoop: self.eventLoop, + logger: self.logger + ) { runtimeClient in + try await Lambda.runLoop( + runtimeClient: runtimeClient, + handler: handler, + logger: self.logger + ) + } + } } } } diff --git a/readme.md b/readme.md index 1b3374b6..1d021921 100644 --- a/readme.md +++ b/readme.md @@ -220,7 +220,7 @@ You can learn how to deploy and invoke this function in [the streaming example R ### Integration with AWS Services - Most Lambda functions are triggered by events originating in other AWS services such as `Amazon SNS`, `Amazon SQS` or `AWS APIGateway`. + Most Lambda functions are triggered by events originating in other AWS services such as Amazon SNS, Amazon SQS or AWS APIGateway. The [Swift AWS Lambda Events](http://github.com/swift-server/swift-aws-lambda-events) package includes an `AWSLambdaEvents` module that provides implementations for most common AWS event types further simplifying writing Lambda functions. @@ -294,6 +294,49 @@ try await runtime.run() You can learn how to deploy and invoke this function in [the background tasks example README file](Examples/BackgroundTasks/README.md). +## Testing Locally + +Before deploying your code to AWS Lambda, you can test it locally by running the executable target on your local machine. It will look like this on CLI: + +```sh +swift run +``` + +When not running inside a Lambda execution environment, it starts a local HTTP server listening on port 7000. You can invoke your local Lambda function by sending an HTTP POST request to `http://127.0.0.1:7000/invoke`. + +The request must include the JSON payload expected as an `event` by your function. You can create a text file with the JSON payload documented by AWS or captured from a trace. In this example, we used [the APIGatewayv2 JSON payload from the documentation](https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html#apigateway-example-event), saved as `events/create-session.json` text file. + +Then we use curl to invoke the local endpoint with the test JSON payload. + +```sh +curl -v --header "Content-Type:\ application/json" --data @events/create-session.json http://127.0.0.1:7000/invoke +* Trying 127.0.0.1:7000... +* Connected to 127.0.0.1 (127.0.0.1) port 7000 +> POST /invoke HTTP/1.1 +> Host: 127.0.0.1:7000 +> User-Agent: curl/8.4.0 +> Accept: */* +> Content-Type:\ application/json +> Content-Length: 1160 +> +< HTTP/1.1 200 OK +< content-length: 247 +< +* 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 + +By default, when using the local Lambda server, it listens on the `/invoke` 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. In that case, you can use the `LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT` environment variable to force the runtime to listen on a different endpoint. + +Example: + +```sh +LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT=/2015-03-31/functions/function/invocations swift run +``` + ## Deploying your Swift Lambda functions