aws/lambda
AWS Lambda custom-runtime support.
BEAM has no AWS-managed Lambda runtime, so a Gleam function deployed to
Lambda has to implement the Lambda Runtime API itself: a small
HTTP contract spoken over the loopback endpoint Lambda advertises in
AWS_LAMBDA_RUNTIME_API. This module is that implementation.
The lifecycle is a loop:
GET /runtime/invocation/next— long-poll for the next event. The response body is the raw event payload and the response headers carry the per-invocationContext.- Run the handler against the payload + context.
POST /runtime/invocation/{id}/responseon success, orPOST /runtime/invocation/{id}/erroron failure.- Repeat.
Write main as:
import aws/lambda
import gleam/bit_array
pub fn main() {
lambda.start(fn(payload, _context) {
let assert Ok(text) = bit_array.to_string(payload)
Ok(bit_array.from_string("hello, " <> text))
})
}
For typed events and JSON responses use start_json
together with the envelopes in aws/lambda/event and the responses in
aws/lambda/response.
start blocks forever, so it only returns when the runtime cannot
continue — either because the process is not running under a Lambda
runtime (NotRunningInLambda) or because the Runtime API became
unreachable. The returned RuntimeError says which.
A handler that raises (a panic, a failed let assert, or any Erlang
exception) does not take the process down: the runtime traps it, reports
it to /error as an "Unhandled" failure, and serves the next event.
Deploying
Lambda ships no managed BEAM runtime, so deploy as an OS-only custom
runtime (provided.al2023). The package needs an executable bootstrap
at its root that boots the VM and runs your main; for an Erlang release
built against Amazon Linux 2023 that is roughly:
#!/bin/sh
set -eu
exec "${LAMBDA_TASK_ROOT}/bin/my_app" foreground
In that process Lambda sets AWS_LAMBDA_RUNTIME_API (the endpoint this
module talks to), plus _HANDLER and LAMBDA_TASK_ROOT. The handler is
whatever your main passes to start, so _HANDLER is unused.
Types
Low-level Runtime API client: the HTTP sender plus the
AWS_LAMBDA_RUNTIME_API endpoint (a bare host:port, no scheme). Build
one with api_from_env, or by hand for testing.
pub type Api {
Api(
send: fn(request.Request(BitArray)) -> Result(
response.Response(BitArray),
http_send.HttpError,
),
endpoint: String,
)
}
Constructors
-
Api( send: fn(request.Request(BitArray)) -> Result( response.Response(BitArray), http_send.HttpError, ), endpoint: String, )
Per-invocation context, populated from the Lambda-Runtime-* response
headers of the next-invocation call.
pub type Context {
Context(
request_id: String,
deadline_ms: Int,
invoked_function_arn: String,
trace_id: option.Option(String),
client_context: option.Option(String),
cognito_identity: option.Option(String),
)
}
Constructors
-
Context( request_id: String, deadline_ms: Int, invoked_function_arn: String, trace_id: option.Option(String), client_context: option.Option(String), cognito_identity: option.Option(String), )Arguments
- request_id
-
Lambda-Runtime-Aws-Request-Id: identifies this invocation. Echoed back in the/responseand/errorURLs. - deadline_ms
-
Lambda-Runtime-Deadline-Ms: the wall-clock time the invocation times out, in Unix epoch milliseconds.0if the header was absent. - invoked_function_arn
-
Lambda-Runtime-Invoked-Function-Arn: the ARN of the function, version, or alias the caller invoked. - trace_id
-
Lambda-Runtime-Trace-Id: the X-Ray tracing header. The runtime copies this into the_X_AMZ_TRACE_IDenvironment variable before calling the handler so AWS SDK calls join the active trace. - client_context
-
Lambda-Runtime-Client-Context: base64 client context, set only for invocations from the AWS Mobile SDK. - cognito_identity
-
Lambda-Runtime-Cognito-Identity: Cognito identity, set only for invocations from the AWS Mobile SDK.
A raw handler: raw event bytes and context in, response bytes or an
InvocationError out.
pub type Handler =
fn(BitArray, Context) -> Result(BitArray, InvocationError)
What rescue_call reports when a handler raises rather than returning.
pub type HandlerCrash {
HandlerCrash(
class: String,
message: String,
stack_trace: List(String),
)
}
Constructors
-
HandlerCrash( class: String, message: String, stack_trace: List(String), )
The result of polling /runtime/invocation/next: the raw event payload
plus its context. The payload is bytes — Lambda delivers every trigger
(SQS, API Gateway, EventBridge, S3, …) as a JSON document, but the
runtime does not assume UTF-8 so binary custom payloads pass through.
pub type Invocation {
Invocation(context: Context, payload: BitArray)
}
Constructors
-
Invocation(context: Context, payload: BitArray)
A failure to report back to Lambda for a single invocation. Serialised to
the { "errorType", "errorMessage", "stackTrace" } body the Runtime API
expects, and error_type is also sent in the
Lambda-Runtime-Function-Error-Type header.
pub type InvocationError {
InvocationError(
error_type: String,
error_message: String,
stack_trace: List(String),
)
}
Constructors
-
InvocationError( error_type: String, error_message: String, stack_trace: List(String), )
A fatal error in the runtime loop itself — distinct from an
InvocationError, which concerns one event. These stop serve/start.
pub type RuntimeError {
NotRunningInLambda
InvalidEndpoint(endpoint: String)
Transport(http_send.HttpError)
MissingRequestId
UnexpectedStatus(endpoint: String, status: Int)
}
Constructors
-
NotRunningInLambdaAWS_LAMBDA_RUNTIME_APIwas not set: the process is not running under a Lambda execution environment. -
InvalidEndpoint(endpoint: String)AWS_LAMBDA_RUNTIME_APIdid not form a valid URL. -
Transport(http_send.HttpError)The HTTP transport failed talking to the Runtime API.
-
MissingRequestIdA next-invocation response arrived without the required
Lambda-Runtime-Aws-Request-Idheader. -
UnexpectedStatus(endpoint: String, status: Int)The Runtime API answered with an unexpected status code.
endpointnames the call (“next”, “response”, “error”, “init/error”).
Values
pub fn api_from_env() -> Result(Api, RuntimeError)
Build an Api from AWS_LAMBDA_RUNTIME_API. The sender is tuned
for the long-poll on /next: Lambda may freeze the process between
events and hold the connection open up to the 15-minute function ceiling.
pub fn invocation_error(
error_type: String,
message: String,
) -> InvocationError
Build an InvocationError with an empty stack trace.
pub fn json_handler(
decoder: decode.Decoder(event),
handler: fn(event, Context) -> Result(response, String),
encode: fn(response) -> json.Json,
) -> fn(BitArray, Context) -> Result(BitArray, InvocationError)
pub fn next(api: Api) -> Result(Invocation, RuntimeError)
Poll GET /runtime/invocation/next for the next event. Blocks until
Lambda has an invocation to deliver.
pub fn process_invocation(
api: Api,
set_trace_id: fn(String) -> Nil,
handler: fn(BitArray, Context) -> Result(
BitArray,
InvocationError,
),
) -> Result(Nil, RuntimeError)
One turn of the loop: poll /next, propagate the trace id, run the
handler (trapping exceptions), then post the response or the error.
Ok(Nil) means the invocation was fully handled — including the case
where the handler failed but the /error post succeeded. Error is
reserved for Runtime API failures.
pub fn send_error(
api: Api,
request_id: String,
error: InvocationError,
) -> Result(Nil, RuntimeError)
POST /runtime/invocation/{request_id}/error with the JSON error body and
the Lambda-Runtime-Function-Error-Type header.
pub fn send_init_error(
api: Api,
error: InvocationError,
) -> Result(Nil, RuntimeError)
POST /runtime/init/error to report a fatal initialization failure
before the first invocation is polled.
pub fn send_response(
api: Api,
request_id: String,
body: BitArray,
) -> Result(Nil, RuntimeError)
POST /runtime/invocation/{request_id}/response with the result bytes.
pub fn serve(
api: Api,
set_trace_id: fn(String) -> Nil,
handler: fn(BitArray, Context) -> Result(
BitArray,
InvocationError,
),
) -> RuntimeError
Process invocations forever. Returns the first RuntimeError that stops
the loop (Runtime API unreachable, protocol violation, …). A handler
that errors or raises does not stop the loop — that failure is reported
to /error and the loop continues.
set_trace_id is called with the X-Ray trace id before each handler
invocation; start wires it to set _X_AMZ_TRACE_ID.
pub fn start(
handler: fn(BitArray, Context) -> Result(
BitArray,
InvocationError,
),
) -> RuntimeError
Run the Lambda runtime loop with a raw bytes handler. Reads
AWS_LAMBDA_RUNTIME_API, then polls/handles/responds forever. Returns
only on a fatal RuntimeError.
pub fn start_json(
decoder: decode.Decoder(event),
handler: fn(event, Context) -> Result(response, String),
encode: fn(response) -> json.Json,
) -> RuntimeError
Run the loop with a typed JSON handler. The event payload is decoded with
decoder; the handler’s Ok value is encoded with encode and posted
as the response; the handler’s Error string is reported to Lambda. A
payload that fails to decode is reported as a Runtime.InvalidEvent
error without invoking the handler.
import aws/lambda
import aws/lambda/event
import aws/lambda/response
pub fn main() {
lambda.start_json(
event.api_gateway_v2_decoder(),
fn(req, _ctx) { Ok(response.proxy_response(200, "hi " <> req.raw_path)) },
response.proxy_to_json,
)
}