-module(aws@lambda). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/aws/lambda.gleam"). -export([send_error/3, send_response/3, next/1, process_invocation/3, serve/3, api_from_env/0, start/1, local_event_from/2, context_default/0, invoke_once/2, run/1, invocation_error/2, json_handler/3, start_json/3, send_init_error/2, get_env/1]). -export_type([context/0, invocation/0, invocation_error/0, runtime_error/0, handler_crash/0, api/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " AWS Lambda custom-runtime support.\n" "\n" " BEAM has no AWS-managed Lambda runtime, so a Gleam function deployed to\n" " Lambda has to implement the [Lambda Runtime API][api] itself: a small\n" " HTTP contract spoken over the loopback endpoint Lambda advertises in\n" " `AWS_LAMBDA_RUNTIME_API`. This module is that implementation.\n" "\n" " The lifecycle is a loop:\n" "\n" " 1. `GET /runtime/invocation/next` — long-poll for the next event.\n" " The response body is the raw event payload and the response\n" " headers carry the per-invocation [`Context`](#Context).\n" " 2. Run the handler against the payload + context.\n" " 3. `POST /runtime/invocation/{id}/response` on success, or\n" " `POST /runtime/invocation/{id}/error` on failure.\n" " 4. Repeat.\n" "\n" " Write `main` as:\n" "\n" " ```gleam\n" " import aws/lambda\n" " import gleam/bit_array\n" "\n" " pub fn main() {\n" " lambda.start(fn(payload, _context) {\n" " let assert Ok(text) = bit_array.to_string(payload)\n" " Ok(bit_array.from_string(\"hello, \" <> text))\n" " })\n" " }\n" " ```\n" "\n" " For typed events and JSON responses use [`start_json`](#start_json)\n" " together with the envelopes in `aws/lambda/event` and the responses in\n" " `aws/lambda/response`.\n" "\n" " `start` blocks forever, so it only returns when the runtime cannot\n" " continue — either because the process is not running under a Lambda\n" " runtime (`NotRunningInLambda`) or because the Runtime API became\n" " unreachable. The returned [`RuntimeError`](#RuntimeError) says which.\n" "\n" " A handler that raises (a `panic`, a failed `let assert`, or any Erlang\n" " exception) does not take the process down: the runtime traps it, reports\n" " it to `/error` as an `\"Unhandled\"` failure, and serves the next event.\n" "\n" " ## Deploying\n" "\n" " Lambda ships no managed BEAM runtime, so deploy as an OS-only custom\n" " runtime (`provided.al2023`). The package needs an executable `bootstrap`\n" " at its root that boots the VM and runs your `main`; for an Erlang release\n" " built against Amazon Linux 2023 that is roughly:\n" "\n" " ```sh\n" " #!/bin/sh\n" " set -eu\n" " exec \"${LAMBDA_TASK_ROOT}/bin/my_app\" foreground\n" " ```\n" "\n" " In that process Lambda sets `AWS_LAMBDA_RUNTIME_API` (the endpoint this\n" " module talks to), plus `_HANDLER` and `LAMBDA_TASK_ROOT`. The handler is\n" " whatever your `main` passes to `start`, so `_HANDLER` is unused.\n" "\n" " [api]: https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html\n" ). -type context() :: {context, binary(), integer(), binary(), gleam@option:option(binary()), gleam@option:option(binary()), gleam@option:option(binary())}. -type invocation() :: {invocation, context(), bitstring()}. -type invocation_error() :: {invocation_error, binary(), binary(), list(binary())}. -type runtime_error() :: not_running_in_lambda | {invalid_endpoint, binary()} | {transport, aws@internal@http_send:http_error()} | missing_request_id | {unexpected_status, binary(), integer()}. -type handler_crash() :: {handler_crash, binary(), binary(), list(binary())}. -type api() :: {api, fun((gleam@http@request:request(bitstring())) -> {ok, gleam@http@response:response(bitstring())} | {error, aws@internal@http_send:http_error()}), binary()}. -file("src/aws/lambda.gleam", 612). -spec set_xray_trace_id(binary()) -> nil. set_xray_trace_id(Trace_id) -> aws_ffi:set_env(<<"_X_AMZ_TRACE_ID"/utf8>>, Trace_id). -file("src/aws/lambda.gleam", 402). -spec describe_runtime_error(runtime_error()) -> binary(). describe_runtime_error(Error) -> case Error of not_running_in_lambda -> <<"not running under a Lambda runtime"/utf8>>; {invalid_endpoint, Endpoint} -> <<"invalid runtime endpoint: "/utf8, Endpoint/binary>>; {transport, _} -> <<"transport failure talking to the Runtime API"/utf8>>; missing_request_id -> <<"next-invocation response missing request id"/utf8>>; {unexpected_status, Endpoint@1, Status} -> <<<<<<"unexpected status "/utf8, (erlang:integer_to_binary(Status))/binary>>/binary, " from "/utf8>>/binary, Endpoint@1/binary>> end. -file("src/aws/lambda.gleam", 563). -spec send(api(), gleam@http@request:request(bitstring())) -> {ok, gleam@http@response:response(bitstring())} | {error, runtime_error()}. send(Api, Request) -> _pipe = (erlang:element(2, Api))(Request), gleam@result:map_error(_pipe, fun(Field@0) -> {transport, Field@0} end). -file("src/aws/lambda.gleam", 570). -spec expect_accepted(api(), binary(), gleam@http@request:request(bitstring())) -> {ok, nil} | {error, runtime_error()}. expect_accepted(Api, Endpoint, Request) -> gleam@result:'try'( send(Api, Request), fun(Response) -> case (erlang:element(2, Response) >= 200) andalso (erlang:element( 2, Response ) < 300) of true -> {ok, nil}; false -> {error, {unexpected_status, Endpoint, erlang:element(2, Response)}} end end ). -file("src/aws/lambda.gleam", 528). -spec error_headers(invocation_error()) -> list({binary(), binary()}). error_headers(Error) -> [{<<"lambda-runtime-function-error-type"/utf8>>, erlang:element(2, Error)}, {<<"content-type"/utf8>>, <<"application/json"/utf8>>}]. -file("src/aws/lambda.gleam", 535). -spec error_body(invocation_error()) -> bitstring(). error_body(Error) -> _pipe = gleam@json:object( [{<<"errorType"/utf8>>, gleam@json:string(erlang:element(2, Error))}, {<<"errorMessage"/utf8>>, gleam@json:string(erlang:element(3, Error))}, {<<"stackTrace"/utf8>>, gleam@json:array( erlang:element(4, Error), fun gleam@json:string/1 )}] ), _pipe@1 = gleam@json:to_string(_pipe), gleam_stdlib:identity(_pipe@1). -file("src/aws/lambda.gleam", 601). -spec apply_headers( gleam@http@request:request(bitstring()), list({binary(), binary()}) ) -> gleam@http@request:request(bitstring()). apply_headers(Request, Headers) -> case Headers of [] -> Request; [{Key, Value} | Rest] -> apply_headers( gleam@http@request:set_header(Request, Key, Value), Rest ) end. -file("src/aws/lambda.gleam", 583). -spec build_request( gleam@http:method(), api(), binary(), bitstring(), list({binary(), binary()}) ) -> {ok, gleam@http@request:request(bitstring())} | {error, runtime_error()}. build_request(Method, Api, Path, Body, Headers) -> Url = <<<<<<<<"http://"/utf8, (erlang:element(3, Api))/binary>>/binary, "/"/utf8>>/binary, "2018-06-01"/utf8>>/binary, Path/binary>>, gleam@result:'try'( begin _pipe = gleam@http@request:to(Url), gleam@result:replace_error( _pipe, {invalid_endpoint, erlang:element(3, Api)} ) end, fun(Base) -> _pipe@1 = Base, _pipe@2 = gleam@http@request:set_method(_pipe@1, Method), _pipe@3 = gleam@http@request:set_body(_pipe@2, Body), _pipe@4 = apply_headers(_pipe@3, Headers), {ok, _pipe@4} end ). -file("src/aws/lambda.gleam", 449). ?DOC( " `POST /runtime/invocation/{request_id}/error` with the JSON error body and\n" " the `Lambda-Runtime-Function-Error-Type` header.\n" ). -spec send_error(api(), binary(), invocation_error()) -> {ok, nil} | {error, runtime_error()}. send_error(Api, Request_id, Error) -> gleam@result:'try'( build_request( post, Api, <<<<"/runtime/invocation/"/utf8, Request_id/binary>>/binary, "/error"/utf8>>, error_body(Error), error_headers(Error) ), fun(Request) -> expect_accepted(Api, <<"error"/utf8>>, Request) end ). -file("src/aws/lambda.gleam", 430). ?DOC(" `POST /runtime/invocation/{request_id}/response` with the result bytes.\n"). -spec send_response(api(), binary(), bitstring()) -> {ok, nil} | {error, runtime_error()}. send_response(Api, Request_id, Body) -> gleam@result:'try'( build_request( post, Api, <<<<"/runtime/invocation/"/utf8, Request_id/binary>>/binary, "/response"/utf8>>, Body, [] ), fun(Request) -> expect_accepted(Api, <<"response"/utf8>>, Request) end ). -file("src/aws/lambda.gleam", 394). -spec crash_to_error(handler_crash()) -> invocation_error(). crash_to_error(Crash) -> {invocation_error, <<"Unhandled"/utf8>>, <<<<(erlang:element(2, Crash))/binary, ": "/utf8>>/binary, (erlang:element(3, Crash))/binary>>, erlang:element(4, Crash)}. -file("src/aws/lambda.gleam", 383). ?DOC( " Invoke the handler with exception trapping. A raised exception becomes an\n" " `\"Unhandled\"` `InvocationError` carrying the class, message, and stack.\n" ). -spec run_handler( fun((bitstring(), context()) -> {ok, bitstring()} | {error, invocation_error()}), bitstring(), context() ) -> {ok, bitstring()} | {error, invocation_error()}. run_handler(Handler, Payload, Context) -> case aws_ffi:rescue_call(fun() -> Handler(Payload, Context) end) of {ok, Result} -> Result; {error, Crash} -> {error, crash_to_error(Crash)} end. -file("src/aws/lambda.gleam", 511). -spec optional_header(gleam@http@response:response(bitstring()), binary()) -> gleam@option:option(binary()). optional_header(Response, Name) -> gleam@option:from_result(gleam@http@response:get_header(Response, Name)). -file("src/aws/lambda.gleam", 518). -spec header_or_empty(gleam@http@response:response(bitstring()), binary()) -> binary(). header_or_empty(Response, Name) -> _pipe = gleam@http@response:get_header(Response, Name), gleam@result:unwrap(_pipe, <<""/utf8>>). -file("src/aws/lambda.gleam", 522). -spec header_int(gleam@http@response:response(bitstring()), binary()) -> integer(). header_int(Response, Name) -> _pipe = gleam@http@response:get_header(Response, Name), _pipe@1 = gleam@result:'try'(_pipe, fun gleam_stdlib:parse_int/1), gleam@result:unwrap(_pipe@1, 0). -file("src/aws/lambda.gleam", 482). -spec parse_invocation(gleam@http@response:response(bitstring())) -> {ok, invocation()} | {error, runtime_error()}. parse_invocation(Response) -> case gleam@http@response:get_header( Response, <<"lambda-runtime-aws-request-id"/utf8>> ) of {error, _} -> {error, missing_request_id}; {ok, Request_id} -> Context = {context, Request_id, header_int(Response, <<"lambda-runtime-deadline-ms"/utf8>>), header_or_empty( Response, <<"lambda-runtime-invoked-function-arn"/utf8>> ), optional_header(Response, <<"lambda-runtime-trace-id"/utf8>>), optional_header( Response, <<"lambda-runtime-client-context"/utf8>> ), optional_header( Response, <<"lambda-runtime-cognito-identity"/utf8>> )}, {ok, {invocation, Context, erlang:element(4, Response)}} end. -file("src/aws/lambda.gleam", 418). ?DOC( " Poll `GET /runtime/invocation/next` for the next event. Blocks until\n" " Lambda has an invocation to deliver.\n" ). -spec next(api()) -> {ok, invocation()} | {error, runtime_error()}. next(Api) -> gleam@result:'try'( build_request(get, Api, <<"/runtime/invocation/next"/utf8>>, <<>>, []), fun(Request) -> gleam@result:'try'( send(Api, Request), fun(Response) -> case erlang:element(2, Response) of 200 -> parse_invocation(Response); Status -> {error, {unexpected_status, <<"next"/utf8>>, Status}} end end ) end ). -file("src/aws/lambda.gleam", 347). ?DOC( " One turn of the loop: poll `/next`, propagate the trace id, run the\n" " handler (trapping exceptions), then post the response or the error.\n" " `Ok(Nil)` means the invocation was fully handled — including the case\n" " where the handler failed but the `/error` post succeeded. `Error` is\n" " reserved for Runtime API failures.\n" ). -spec process_invocation( api(), fun((binary()) -> nil), fun((bitstring(), context()) -> {ok, bitstring()} | {error, invocation_error()}) ) -> {ok, nil} | {error, runtime_error()}. process_invocation(Api, Set_trace_id, Handler) -> gleam@result:'try'( next(Api), fun(Invocation) -> Context = erlang:element(2, Invocation), aws@internal@log:debug( fun() -> <<<<<<<<"aws lambda: invocation "/utf8, (erlang:element(2, Context))/binary>>/binary, " ("/utf8>>/binary, (erlang:integer_to_binary( erlang:byte_size(erlang:element(3, Invocation)) ))/binary>>/binary, " bytes)"/utf8>> end ), case erlang:element(5, Context) of {some, Trace_id} -> Set_trace_id(Trace_id); none -> nil end, case run_handler(Handler, erlang:element(3, Invocation), Context) of {ok, Body} -> send_response(Api, erlang:element(2, Context), Body); {error, Error} -> aws@internal@log:debug( fun() -> <<<<<<<<<<"aws lambda: invocation "/utf8, (erlang:element(2, Context))/binary>>/binary, " handler error "/utf8>>/binary, (erlang:element(2, Error))/binary>>/binary, ": "/utf8>>/binary, (erlang:element(3, Error))/binary>> end ), send_error(Api, erlang:element(2, Context), Error) end end ). -file("src/aws/lambda.gleam", 326). ?DOC( " Process invocations forever. Returns the first `RuntimeError` that stops\n" " the loop (Runtime API unreachable, protocol violation, ...). A handler\n" " that errors or raises does *not* stop the loop — that failure is reported\n" " to `/error` and the loop continues.\n" "\n" " `set_trace_id` is called with the X-Ray trace id before each handler\n" " invocation; [`start`](#start) wires it to set `_X_AMZ_TRACE_ID`.\n" ). -spec serve( api(), fun((binary()) -> nil), fun((bitstring(), context()) -> {ok, bitstring()} | {error, invocation_error()}) ) -> runtime_error(). serve(Api, Set_trace_id, Handler) -> case process_invocation(Api, Set_trace_id, Handler) of {ok, nil} -> serve(Api, Set_trace_id, Handler); {error, Error} -> aws@internal@log:error( <<"aws lambda runtime: fatal — "/utf8, (describe_runtime_error(Error))/binary>> ), Error end. -file("src/aws/lambda.gleam", 309). ?DOC( " Build an [`Api`](#Api) from `AWS_LAMBDA_RUNTIME_API`. The sender is tuned\n" " for the long-poll on `/next`: Lambda may freeze the process between\n" " events and hold the connection open up to the 15-minute function ceiling.\n" ). -spec api_from_env() -> {ok, api()} | {error, runtime_error()}. api_from_env() -> case aws_ffi:get_env(<<"AWS_LAMBDA_RUNTIME_API"/utf8>>) of {ok, Endpoint} -> {ok, {api, aws@internal@http_send:with_timeout(900), Endpoint}}; {error, _} -> {error, not_running_in_lambda} end. -file("src/aws/lambda.gleam", 168). ?DOC( " Run the Lambda runtime loop with a raw bytes handler. Reads\n" " `AWS_LAMBDA_RUNTIME_API`, then polls/handles/responds forever. Returns\n" " only on a fatal `RuntimeError`.\n" ). -spec start( fun((bitstring(), context()) -> {ok, bitstring()} | {error, invocation_error()}) ) -> runtime_error(). start(Handler) -> case api_from_env() of {ok, Api} -> serve(Api, fun set_xray_trace_id/1, Handler); {error, Error} -> Error end. -file("src/aws/lambda.gleam", 241). -spec event_arg(list(binary())) -> {ok, binary()} | {error, nil}. event_arg(Args) -> case Args of [<<"--event"/utf8>>, Value | _] -> {ok, Value}; [<<"-e"/utf8>>, Value | _] -> {ok, Value}; [_ | Rest] -> event_arg(Rest); [] -> {error, nil} end. -file("src/aws/lambda.gleam", 227). ?DOC( " Resolve the local event payload: an `--event`/`-e` argument, else\n" " `$LAMBDA_EVENT`, else `{}`. Inputs are injected so it stays testable;\n" " `run` calls it with the real argv and environment.\n" ). -spec local_event_from( list(binary()), fun((binary()) -> {ok, binary()} | {error, nil}) ) -> binary(). local_event_from(Args, Read_env) -> case event_arg(Args) of {ok, Json} -> Json; {error, _} -> gleam@result:unwrap( Read_env(<<"LAMBDA_EVENT"/utf8>>), <<"{}"/utf8>> ) end. -file("src/aws/lambda.gleam", 237). -spec local_event() -> binary(). local_event() -> local_event_from(aws_ffi:plain_args(), fun aws_ffi:get_env/1). -file("src/aws/lambda.gleam", 213). ?DOC( " A `Context` with local-friendly placeholder values, for `run`'s local\n" " path and for tests.\n" ). -spec context_default() -> context(). context_default() -> {context, <<"local"/utf8>>, 0, <<"arn:aws:lambda:local:000000000000:function:local"/utf8>>, none, none, none}. -file("src/aws/lambda.gleam", 204). ?DOC( " Invoke the handler once against a raw payload, trapping a panic exactly as\n" " the cloud loop does. The testable core of local execution — no I/O and no\n" " process exit. Uses [`context_default`](#context_default) for the context.\n" ). -spec invoke_once( fun((bitstring(), context()) -> {ok, bitstring()} | {error, invocation_error()}), bitstring() ) -> {ok, bitstring()} | {error, invocation_error()}. invoke_once(Handler, Payload) -> run_handler(Handler, Payload, context_default()). -file("src/aws/lambda.gleam", 184). ?DOC( " Run the handler from the same `main`, locally or in the cloud. Under\n" " Lambda (`AWS_LAMBDA_RUNTIME_API` set) this is [`start`](#start) — the\n" " poll-forever loop. Run any other way (e.g. `gleam run`) it invokes the\n" " handler exactly once, prints the response to stdout, and exits the VM —\n" " so you can smoke-test before deploying. The local event is read from\n" " `--event ` (or `-e`), then `$LAMBDA_EVENT`, then `{}`.\n" "\n" " Because it can exit the VM, `run` is meant to be your `main`; for the\n" " cloud-only loop with no local/exit behaviour, call [`start`](#start).\n" ). -spec run( fun((bitstring(), context()) -> {ok, bitstring()} | {error, invocation_error()}) ) -> runtime_error(). run(Handler) -> case api_from_env() of {ok, Api} -> serve(Api, fun set_xray_trace_id/1, Handler); {error, _} -> case invoke_once(Handler, gleam_stdlib:identity(local_event())) of {ok, Body} -> gleam_stdlib:println( gleam@result:unwrap( gleam@bit_array:to_string(Body), <<""/utf8>> ) ), erlang:halt(0); {error, Failure} -> gleam_stdlib:println_error( <<<<(erlang:element(2, Failure))/binary, ": "/utf8>>/binary, (erlang:element(3, Failure))/binary>> ), erlang:halt(1) end end. -file("src/aws/lambda.gleam", 295). ?DOC(" Build an [`InvocationError`](#InvocationError) with an empty stack trace.\n"). -spec invocation_error(binary(), binary()) -> invocation_error(). invocation_error(Error_type, Message) -> {invocation_error, Error_type, Message, []}. -file("src/aws/lambda.gleam", 545). -spec decode_payload(bitstring(), gleam@dynamic@decode:decoder(NUZ)) -> {ok, NUZ} | {error, invocation_error()}. decode_payload(Payload, Decoder) -> gleam@result:'try'( begin _pipe = gleam@bit_array:to_string(Payload), gleam@result:replace_error( _pipe, invocation_error( <<"Runtime.InvalidEvent"/utf8>>, <<"event payload was not valid UTF-8"/utf8>> ) ) end, fun(Text) -> _pipe@1 = gleam@json:parse(Text, Decoder), gleam@result:replace_error( _pipe@1, invocation_error( <<"Runtime.InvalidEvent"/utf8>>, <<"event payload did not match the expected schema"/utf8>> ) ) end ). -file("src/aws/lambda.gleam", 279). ?DOC( " Adapt a typed JSON handler into a raw [`Handler`](#Handler). Useful when\n" " you want to drive the loop yourself via [`serve`](#serve) but still want\n" " the decode/encode plumbing.\n" ). -spec json_handler( gleam@dynamic@decode:decoder(NTY), fun((NTY, context()) -> {ok, NUA} | {error, binary()}), fun((NUA) -> gleam@json:json()) ) -> fun((bitstring(), context()) -> {ok, bitstring()} | {error, invocation_error()}). json_handler(Decoder, Handler, Encode) -> fun(Payload, Context) -> gleam@result:'try'( decode_payload(Payload, Decoder), fun(Event) -> case Handler(Event, Context) of {ok, Response} -> {ok, gleam_stdlib:identity( gleam@json:to_string(Encode(Response)) )}; {error, Message} -> {error, invocation_error(<<"Handler.Error"/utf8>>, Message)} end end ) end. -file("src/aws/lambda.gleam", 268). ?DOC( " Run the loop with a typed JSON handler. The event payload is decoded with\n" " `decoder`; the handler's `Ok` value is encoded with `encode` and posted\n" " as the response; the handler's `Error` string is reported to Lambda. A\n" " payload that fails to decode is reported as a `Runtime.InvalidEvent`\n" " error without invoking the handler.\n" "\n" " ```gleam\n" " import aws/lambda\n" " import aws/lambda/event\n" " import aws/lambda/response\n" "\n" " pub fn main() {\n" " lambda.start_json(\n" " event.api_gateway_v2_decoder(),\n" " fn(req, _ctx) { Ok(response.proxy_response(200, \"hi \" <> req.raw_path)) },\n" " response.proxy_to_json,\n" " )\n" " }\n" " ```\n" ). -spec start_json( gleam@dynamic@decode:decoder(NTT), fun((NTT, context()) -> {ok, NTV} | {error, binary()}), fun((NTV) -> gleam@json:json()) ) -> runtime_error(). start_json(Decoder, Handler, Encode) -> start(json_handler(Decoder, Handler, Encode)). -file("src/aws/lambda.gleam", 466). ?DOC( " `POST /runtime/init/error` to report a fatal initialization failure\n" " before the first invocation is polled.\n" ). -spec send_init_error(api(), invocation_error()) -> {ok, nil} | {error, runtime_error()}. send_init_error(Api, Error) -> gleam@result:'try'( build_request( post, Api, <<"/runtime/init/error"/utf8>>, error_body(Error), error_headers(Error) ), fun(Request) -> expect_accepted(Api, <<"init/error"/utf8>>, Request) end ). -file("src/aws/lambda.gleam", 619). ?DOC( " Deprecated alias for [`aws/env.get_env`](../env.html#get_env). Env access\n" " is not Lambda-specific, so it now lives in `aws/env`.\n" ). -spec get_env(binary()) -> {ok, binary()} | {error, nil}. get_env(Name) -> aws_ffi:get_env(Name).