Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions RELEASE.CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### July 15, 2026
`4.0.2`
- Add `Lambda-Runtime-Invocation-Id` header support for cross-wiring protection. The RIC now echoes the invocation ID received from RAPID on `/next` back on `/response` and `/error`, enabling RAPID to detect and reject stale responses from timed-out invocations.

### June 25, 2026
`4.0.1`
- Support building on Alpine Linux 3.17+ (musl) without `libexecinfo-dev` ([#204](https://github.com/aws/aws-lambda-python-runtime-interface-client/pull/204))
Expand Down
2 changes: 1 addition & 1 deletion awslambdaric/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
"""

__version__ = "4.0.1"
__version__ = "4.0.2"
10 changes: 7 additions & 3 deletions awslambdaric/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
)
AWS_LAMBDA_INITIALIZATION_TYPE = "AWS_LAMBDA_INITIALIZATION_TYPE"
INIT_TYPE_SNAP_START = "snap-start"
PREVIEW_RUNTIME_ENVS = {"AWS_Lambda_python3.15"}


def _get_handler(handler):
Expand Down Expand Up @@ -161,6 +160,7 @@ def handle_event_request(
epoch_deadline_time_in_ms,
tenant_id,
log_sink,
invocation_id=None,
):
error_result = None
try:
Expand Down Expand Up @@ -205,11 +205,11 @@ def handle_event_request(

log_error(error_result, log_sink)
lambda_runtime_client.post_invocation_error(
invoke_id, to_json(error_result), to_json(xray_fault)
invoke_id, to_json(error_result), to_json(xray_fault), invocation_id
)
else:
lambda_runtime_client.post_invocation_result(
invoke_id, result, result_content_type
invoke_id, result, result_content_type, invocation_id
)


Expand Down Expand Up @@ -478,6 +478,9 @@ def _setup_logging(log_format, log_level, log_sink):
logger.addHandler(logger_handler)


PREVIEW_RUNTIME_ENVS = {"AWS_Lambda_python3.15"}


def _log_preview_runtime_warning():
"""Emit a warning if the runtime version is a preview."""
if os.environ.get("LAMBDA_DISABLE_PREVIEW_WARN", ""):
Expand Down Expand Up @@ -545,4 +548,5 @@ def run(handler, lambda_runtime_client):
event_request.deadline_time_in_ms,
event_request.tenant_id,
log_sink,
event_request.invocation_id,
)
16 changes: 13 additions & 3 deletions awslambdaric/lambda_runtime_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,15 @@ def wait_next_invocation(self):
tenant_id=headers.get("Lambda-Runtime-Aws-Tenant-Id"),
content_type=headers.get("Content-Type"),
event_body=response_body,
invocation_id=headers.get("Lambda-Runtime-Invocation-Id"),
)

def post_invocation_result(
self, invoke_id, result_data, content_type="application/json"
self,
invoke_id,
result_data,
content_type="application/json",
invocation_id=None,
):
try:
runtime_client.post_invocation_result(
Expand All @@ -181,17 +186,22 @@ def post_invocation_result(
else result_data.encode("utf-8")
),
content_type,
invocation_id,
)
except Exception as e:
self.handle_exception(e)

def post_invocation_error(self, invoke_id, error_response_data, xray_fault):
def post_invocation_error(
self, invoke_id, error_response_data, xray_fault, invocation_id=None
):
try:
max_header_size = 1024 * 1024
xray_fault = (
xray_fault if len(xray_fault.encode()) < max_header_size else ""
)
runtime_client.post_error(invoke_id, error_response_data, xray_fault)
runtime_client.post_error(
invoke_id, error_response_data, xray_fault, invocation_id
)
except Exception as e:
self.handle_exception(e)

Expand Down
20 changes: 12 additions & 8 deletions awslambdaric/runtime_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ static PyObject *method_next(PyObject *self) {
auto content_type = response.content_type.c_str();
auto cognito_id = response.cognito_identity.c_str();
auto tenant_id = response.tenant_id.c_str();
auto invocation_id = response.invocation_id.c_str();

PyObject *payload_bytes = PyBytes_FromStringAndSize(payload.c_str(), payload.length());
PyObject *result = Py_BuildValue("(O,{s:s,s:s,s:s,s:l,s:s,s:s,s:s,s:s})",
PyObject *result = Py_BuildValue("(O,{s:s,s:s,s:s,s:l,s:s,s:s,s:s,s:s,s:s})",
payload_bytes, //Py_BuildValue() increments reference counter
"Lambda-Runtime-Aws-Request-Id", request_id,
"Lambda-Runtime-Trace-Id", NULL_IF_EMPTY(trace_id),
Expand All @@ -64,7 +65,8 @@ static PyObject *method_next(PyObject *self) {
"Lambda-Runtime-Client-Context", NULL_IF_EMPTY(client_context),
"Content-Type", NULL_IF_EMPTY(content_type),
"Lambda-Runtime-Cognito-Identity", NULL_IF_EMPTY(cognito_id),
"Lambda-Runtime-Aws-Tenant-Id", NULL_IF_EMPTY(tenant_id)
"Lambda-Runtime-Aws-Tenant-Id", NULL_IF_EMPTY(tenant_id),
"Lambda-Runtime-Invocation-Id", NULL_IF_EMPTY(invocation_id)
);

Py_XDECREF(payload_bytes);
Expand All @@ -79,9 +81,9 @@ static PyObject *method_post_invocation_result(PyObject *self, PyObject *args) {

PyObject *invocation_response;
Py_ssize_t length;
char *request_id, *content_type, *response_as_c_string;
char *request_id, *content_type, *response_as_c_string, *invocation_id = nullptr;

if (!PyArg_ParseTuple(args, "sSs", &request_id, &invocation_response, &content_type)) {
if (!PyArg_ParseTuple(args, "sSsz", &request_id, &invocation_response, &content_type, &invocation_id)) {
PyErr_SetString(PyExc_RuntimeError, "Wrong arguments");
return NULL;
}
Expand All @@ -91,7 +93,8 @@ static PyObject *method_post_invocation_result(PyObject *self, PyObject *args) {
std::string response_string(response_as_c_string, response_as_c_string + length);

auto response = aws::lambda_runtime::invocation_response::success(response_string, content_type);
auto outcome = CLIENT->post_success(request_id, response);
std::string inv_id = invocation_id ? invocation_id : "";
auto outcome = CLIENT->post_success(request_id, response, inv_id);
if (!outcome.is_success()) {
PyErr_SetString(PyExc_RuntimeError, "Failed to post invocation response");
return NULL;
Expand All @@ -107,15 +110,16 @@ static PyObject *method_post_error(PyObject *self, PyObject *args) {
return NULL;
}

char *request_id, *response_string, *xray_fault;
char *request_id, *response_string, *xray_fault, *invocation_id = nullptr;

if (!PyArg_ParseTuple(args, "sss", &request_id, &response_string, &xray_fault)) {
if (!PyArg_ParseTuple(args, "sssz", &request_id, &response_string, &xray_fault, &invocation_id)) {
PyErr_SetString(PyExc_RuntimeError, "Wrong arguments");
return NULL;
}

auto response = aws::lambda_runtime::invocation_response(response_string, "application/json", false, xray_fault);
auto outcome = CLIENT->post_failure(request_id, response);
std::string inv_id = invocation_id ? invocation_id : "";
auto outcome = CLIENT->post_failure(request_id, response, inv_id);
if (!outcome.is_success()) {
PyErr_SetString(PyExc_RuntimeError, "Failed to post invocation error");
return NULL;
Expand Down
Binary file modified deps/aws-lambda-cpp-0.2.6.tar.gz
Binary file not shown.
110 changes: 110 additions & 0 deletions deps/patches/aws-lambda-cpp-add-invocation-id.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
diff --git a/include/aws/lambda-runtime/runtime.h b/include/aws/lambda-runtime/runtime.h
--- a/include/aws/lambda-runtime/runtime.h
+++ b/include/aws/lambda-runtime/runtime.h
@@ -67,6 +67,11 @@
std::string tenant_id;

/**
+ * The unique invocation ID for cross-wiring protection.
+ */
+ std::string invocation_id;
+
+ /**
* Function execution deadline counted in milliseconds since the Unix epoch.
*/
std::chrono::time_point<std::chrono::system_clock> deadline;
@@ -184,12 +189,12 @@
/**
* Tells lambda that the function has succeeded.
*/
- post_outcome post_success(std::string const& request_id, invocation_response const& handler_response);
+ post_outcome post_success(std::string const& request_id, invocation_response const& handler_response, std::string const& invocation_id = "");

/**
* Tells lambda that the function has failed.
*/
- post_outcome post_failure(std::string const& request_id, invocation_response const& handler_response);
+ post_outcome post_failure(std::string const& request_id, invocation_response const& handler_response, std::string const& invocation_id = "");

/**
* Tells lambda that the runtime has failed during initialization.
@@ -203,7 +208,8 @@
std::string const& url,
std::string const& content_type,
std::string const& payload,
- std::string const& xray_response);
+ std::string const& xray_response,
+ std::string const& invocation_id = "");

private:
std::string const m_user_agent_header;
diff --git a/src/runtime.cpp b/src/runtime.cpp
--- a/src/runtime.cpp
+++ b/src/runtime.cpp
@@ -41,6 +41,7 @@
static constexpr auto DEADLINE_MS_HEADER = "lambda-runtime-deadline-ms";
static constexpr auto FUNCTION_ARN_HEADER = "lambda-runtime-invoked-function-arn";
static constexpr auto TENANT_ID_HEADER = "lambda-runtime-aws-tenant-id";
+static constexpr auto INVOCATION_ID_HEADER = "lambda-runtime-invocation-id";

enum Endpoints {
INIT,
@@ -294,6 +295,10 @@
req.tenant_id = resp.get_header(TENANT_ID_HEADER);
}

+ if (resp.has_header(INVOCATION_ID_HEADER)) {
+ req.invocation_id = resp.get_header(INVOCATION_ID_HEADER);
+ }
+
if (resp.has_header(DEADLINE_MS_HEADER)) {
auto const& deadline_string = resp.get_header(DEADLINE_MS_HEADER);
constexpr int base = 10;
@@ -310,29 +315,30 @@
return next_outcome(req);
}

-runtime::post_outcome runtime::post_success(std::string const& request_id, invocation_response const& handler_response)
+runtime::post_outcome runtime::post_success(std::string const& request_id, invocation_response const& handler_response, std::string const& invocation_id)
{
std::string const url = m_endpoints[Endpoints::RESULT] + request_id + "/response";
- return do_post(url, handler_response.get_content_type(), handler_response.get_payload(), handler_response.get_xray_response());
+ return do_post(url, handler_response.get_content_type(), handler_response.get_payload(), handler_response.get_xray_response(), invocation_id);
}

-runtime::post_outcome runtime::post_failure(std::string const& request_id, invocation_response const& handler_response)
+runtime::post_outcome runtime::post_failure(std::string const& request_id, invocation_response const& handler_response, std::string const& invocation_id)
{
std::string const url = m_endpoints[Endpoints::RESULT] + request_id + "/error";
- return do_post(url, handler_response.get_content_type(), handler_response.get_payload(), handler_response.get_xray_response());
+ return do_post(url, handler_response.get_content_type(), handler_response.get_payload(), handler_response.get_xray_response(), invocation_id);
}

runtime::post_outcome runtime::post_init_error(runtime_response const& init_error_response)
{
std::string const url = m_endpoints[Endpoints::INIT];
- return do_post(url, init_error_response.get_content_type(), init_error_response.get_payload(), init_error_response.get_xray_response());
+ return do_post(url, init_error_response.get_content_type(), init_error_response.get_payload(), init_error_response.get_xray_response(), "");
}

runtime::post_outcome runtime::do_post(
std::string const& url,
std::string const& content_type,
std::string const& payload,
- std::string const& xray_response)
+ std::string const& xray_response,
+ std::string const& invocation_id)
{
set_curl_post_result_options();
curl_easy_setopt(m_curl_handle, CURLOPT_URL, url.c_str());
@@ -351,6 +357,10 @@
headers = curl_slist_append(headers, "transfer-encoding:");
headers = curl_slist_append(headers, m_user_agent_header.c_str());

+ if (!invocation_id.empty()) {
+ headers = curl_slist_append(headers, ("lambda-runtime-invocation-id: " + invocation_id).c_str());
+ }
+
logging::log_debug(
LOG_TAG, "calculating content length... %s", ("content-length: " + std::to_string(payload.length())).c_str());
headers = curl_slist_append(headers, ("content-length: " + std::to_string(payload.length())).c_str());
21 changes: 21 additions & 0 deletions deps/patches/aws-lambda-cpp-musl-no-execinfo.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -11,10 +11,17 @@
add_library(${PROJECT_NAME}
"src/logging.cpp"
"src/runtime.cpp"
- "src/backward.cpp"
"${CMAKE_CURRENT_BINARY_DIR}/version.cpp"
)

+include(CheckIncludeFileCXX)
+check_include_file_cxx("execinfo.h" HAVE_EXECINFO_H)
+if (HAVE_EXECINFO_H)
+ target_sources(${PROJECT_NAME} PRIVATE "src/backward.cpp")
+else()
+ message("-- execinfo.h not found, stack traces disabled (musl/Alpine Linux)")
+endif()
+
set_target_properties(${PROJECT_NAME} PROPERTIES
SOVERSION 0
VERSION ${PROJECT_VERSION})
4 changes: 3 additions & 1 deletion scripts/update_deps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ wget -c https://github.com/awslabs/aws-lambda-cpp/archive/v$AWS_LAMBDA_CPP_RELEA
patch -p1 < ../patches/aws-lambda-cpp-add-xray-response.patch && \
patch -p1 < ../patches/aws-lambda-cpp-posting-init-errors.patch && \
patch -p1 < ../patches/aws-lambda-cpp-make-the-runtime-client-user-agent-overrideable.patch && \
patch -p1 < ../patches/aws-lambda-cpp-musl-no-execinfo.patch && \
patch -p1 < ../patches/aws-lambda-cpp-make-lto-optional.patch && \
patch -p1 < ../patches/aws-lambda-cpp-add-content-type.patch && \
patch -p1 < ../patches/aws-lambda-cpp-add-tenant-id.patch && \
patch -p1 < ../patches/aws-lambda-cpp-logging-error.patch
patch -p1 < ../patches/aws-lambda-cpp-logging-error.patch && \
patch -p1 < ../patches/aws-lambda-cpp-add-invocation-id.patch
)

## Pack again and remove the folder
Expand Down
8 changes: 5 additions & 3 deletions tests/test_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ def test_handle_event_request_happy_case(self):
"invoke_id",
'{"input": "event_body", "aws_request_id": "invoke_id"}',
"application/json",
None,
)

def test_handle_event_request_invalid_client_context(self):
Expand Down Expand Up @@ -862,7 +863,7 @@ def test_application_json(self):
)

self.lambda_runtime.post_invocation_result.assert_called_once_with(
"invoke-id", '{"response": "foo"}', "application/json"
"invoke-id", '{"response": "foo"}', "application/json", None
)

def test_binary_request_binary_response(self):
Expand All @@ -882,7 +883,7 @@ def test_binary_request_binary_response(self):
)

self.lambda_runtime.post_invocation_result.assert_called_once_with(
"invoke-id", event_body, "application/unknown"
"invoke-id", event_body, "application/unknown", None
)

def test_json_request_binary_response(self):
Expand All @@ -902,7 +903,7 @@ def test_json_request_binary_response(self):
)

self.lambda_runtime.post_invocation_result.assert_called_once_with(
"invoke-id", binary_data, "application/unknown"
"invoke-id", binary_data, "application/unknown", None
)

def test_binary_with_application_json(self):
Expand All @@ -927,6 +928,7 @@ def test_binary_with_application_json(self):
invoke_id,
error_result,
xray_fault,
invocation_id,
), _ = self.lambda_runtime.post_invocation_error.call_args
error_dict = json.loads(error_result)

Expand Down
Loading
Loading