Skip to content

Commit b132afb

Browse files
committed
feat: Add Lambda-Runtime-Invocation-Id support for cross-wiring protection
Echo 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. Fully backward compatible — header only sent when received.
1 parent 46e4c6a commit b132afb

11 files changed

Lines changed: 276 additions & 24 deletions

RELEASE.CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
### July 15, 2026
2+
`4.0.2`
3+
- 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.
4+
15
### June 25, 2026
26
`4.0.1`
37
- Support building on Alpine Linux 3.17+ (musl) without `libexecinfo-dev` ([#204](https://github.com/aws/aws-lambda-python-runtime-interface-client/pull/204))

awslambdaric/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
33
"""
44

5-
__version__ = "4.0.1"
5+
__version__ = "4.0.2"

awslambdaric/bootstrap.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
)
3434
AWS_LAMBDA_INITIALIZATION_TYPE = "AWS_LAMBDA_INITIALIZATION_TYPE"
3535
INIT_TYPE_SNAP_START = "snap-start"
36-
PREVIEW_RUNTIME_ENVS = {"AWS_Lambda_python3.15"}
3736

3837

3938
def _get_handler(handler):
@@ -161,6 +160,7 @@ def handle_event_request(
161160
epoch_deadline_time_in_ms,
162161
tenant_id,
163162
log_sink,
163+
invocation_id=None,
164164
):
165165
error_result = None
166166
try:
@@ -205,11 +205,11 @@ def handle_event_request(
205205

206206
log_error(error_result, log_sink)
207207
lambda_runtime_client.post_invocation_error(
208-
invoke_id, to_json(error_result), to_json(xray_fault)
208+
invoke_id, to_json(error_result), to_json(xray_fault), invocation_id
209209
)
210210
else:
211211
lambda_runtime_client.post_invocation_result(
212-
invoke_id, result, result_content_type
212+
invoke_id, result, result_content_type, invocation_id
213213
)
214214

215215

@@ -478,6 +478,9 @@ def _setup_logging(log_format, log_level, log_sink):
478478
logger.addHandler(logger_handler)
479479

480480

481+
PREVIEW_RUNTIME_ENVS = {"AWS_Lambda_python3.15"}
482+
483+
481484
def _log_preview_runtime_warning():
482485
"""Emit a warning if the runtime version is a preview."""
483486
if os.environ.get("LAMBDA_DISABLE_PREVIEW_WARN", ""):
@@ -545,4 +548,5 @@ def run(handler, lambda_runtime_client):
545548
event_request.deadline_time_in_ms,
546549
event_request.tenant_id,
547550
log_sink,
551+
event_request.invocation_id,
548552
)

awslambdaric/lambda_runtime_client.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,15 @@ def wait_next_invocation(self):
167167
tenant_id=headers.get("Lambda-Runtime-Aws-Tenant-Id"),
168168
content_type=headers.get("Content-Type"),
169169
event_body=response_body,
170+
invocation_id=headers.get("Lambda-Runtime-Invocation-Id"),
170171
)
171172

172173
def post_invocation_result(
173-
self, invoke_id, result_data, content_type="application/json"
174+
self,
175+
invoke_id,
176+
result_data,
177+
content_type="application/json",
178+
invocation_id=None,
174179
):
175180
try:
176181
runtime_client.post_invocation_result(
@@ -181,17 +186,22 @@ def post_invocation_result(
181186
else result_data.encode("utf-8")
182187
),
183188
content_type,
189+
invocation_id,
184190
)
185191
except Exception as e:
186192
self.handle_exception(e)
187193

188-
def post_invocation_error(self, invoke_id, error_response_data, xray_fault):
194+
def post_invocation_error(
195+
self, invoke_id, error_response_data, xray_fault, invocation_id=None
196+
):
189197
try:
190198
max_header_size = 1024 * 1024
191199
xray_fault = (
192200
xray_fault if len(xray_fault.encode()) < max_header_size else ""
193201
)
194-
runtime_client.post_error(invoke_id, error_response_data, xray_fault)
202+
runtime_client.post_error(
203+
invoke_id, error_response_data, xray_fault, invocation_id
204+
)
195205
except Exception as e:
196206
self.handle_exception(e)
197207

awslambdaric/runtime_client.cpp

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,10 @@ static PyObject *method_next(PyObject *self) {
5353
auto content_type = response.content_type.c_str();
5454
auto cognito_id = response.cognito_identity.c_str();
5555
auto tenant_id = response.tenant_id.c_str();
56+
auto invocation_id = response.invocation_id.c_str();
5657

5758
PyObject *payload_bytes = PyBytes_FromStringAndSize(payload.c_str(), payload.length());
58-
PyObject *result = Py_BuildValue("(O,{s:s,s:s,s:s,s:l,s:s,s:s,s:s,s:s})",
59+
PyObject *result = Py_BuildValue("(O,{s:s,s:s,s:s,s:l,s:s,s:s,s:s,s:s,s:s})",
5960
payload_bytes, //Py_BuildValue() increments reference counter
6061
"Lambda-Runtime-Aws-Request-Id", request_id,
6162
"Lambda-Runtime-Trace-Id", NULL_IF_EMPTY(trace_id),
@@ -64,7 +65,8 @@ static PyObject *method_next(PyObject *self) {
6465
"Lambda-Runtime-Client-Context", NULL_IF_EMPTY(client_context),
6566
"Content-Type", NULL_IF_EMPTY(content_type),
6667
"Lambda-Runtime-Cognito-Identity", NULL_IF_EMPTY(cognito_id),
67-
"Lambda-Runtime-Aws-Tenant-Id", NULL_IF_EMPTY(tenant_id)
68+
"Lambda-Runtime-Aws-Tenant-Id", NULL_IF_EMPTY(tenant_id),
69+
"Lambda-Runtime-Invocation-Id", NULL_IF_EMPTY(invocation_id)
6870
);
6971

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

8082
PyObject *invocation_response;
8183
Py_ssize_t length;
82-
char *request_id, *content_type, *response_as_c_string;
84+
char *request_id, *content_type, *response_as_c_string, *invocation_id = nullptr;
8385

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

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

110-
char *request_id, *response_string, *xray_fault;
113+
char *request_id, *response_string, *xray_fault, *invocation_id = nullptr;
111114

112-
if (!PyArg_ParseTuple(args, "sss", &request_id, &response_string, &xray_fault)) {
115+
if (!PyArg_ParseTuple(args, "sssz", &request_id, &response_string, &xray_fault, &invocation_id)) {
113116
PyErr_SetString(PyExc_RuntimeError, "Wrong arguments");
114117
return NULL;
115118
}
116119

117120
auto response = aws::lambda_runtime::invocation_response(response_string, "application/json", false, xray_fault);
118-
auto outcome = CLIENT->post_failure(request_id, response);
121+
std::string inv_id = invocation_id ? invocation_id : "";
122+
auto outcome = CLIENT->post_failure(request_id, response, inv_id);
119123
if (!outcome.is_success()) {
120124
PyErr_SetString(PyExc_RuntimeError, "Failed to post invocation error");
121125
return NULL;

deps/aws-lambda-cpp-0.2.6.tar.gz

177 Bytes
Binary file not shown.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
diff --git a/include/aws/lambda-runtime/runtime.h b/include/aws/lambda-runtime/runtime.h
2+
--- a/include/aws/lambda-runtime/runtime.h
3+
+++ b/include/aws/lambda-runtime/runtime.h
4+
@@ -67,6 +67,11 @@
5+
std::string tenant_id;
6+
7+
/**
8+
+ * The unique invocation ID for cross-wiring protection.
9+
+ */
10+
+ std::string invocation_id;
11+
+
12+
+ /**
13+
* Function execution deadline counted in milliseconds since the Unix epoch.
14+
*/
15+
std::chrono::time_point<std::chrono::system_clock> deadline;
16+
@@ -184,12 +189,12 @@
17+
/**
18+
* Tells lambda that the function has succeeded.
19+
*/
20+
- post_outcome post_success(std::string const& request_id, invocation_response const& handler_response);
21+
+ post_outcome post_success(std::string const& request_id, invocation_response const& handler_response, std::string const& invocation_id = "");
22+
23+
/**
24+
* Tells lambda that the function has failed.
25+
*/
26+
- post_outcome post_failure(std::string const& request_id, invocation_response const& handler_response);
27+
+ post_outcome post_failure(std::string const& request_id, invocation_response const& handler_response, std::string const& invocation_id = "");
28+
29+
/**
30+
* Tells lambda that the runtime has failed during initialization.
31+
@@ -203,7 +208,8 @@
32+
std::string const& url,
33+
std::string const& content_type,
34+
std::string const& payload,
35+
- std::string const& xray_response);
36+
+ std::string const& xray_response,
37+
+ std::string const& invocation_id = "");
38+
39+
private:
40+
std::string const m_user_agent_header;
41+
diff --git a/src/runtime.cpp b/src/runtime.cpp
42+
--- a/src/runtime.cpp
43+
+++ b/src/runtime.cpp
44+
@@ -41,6 +41,7 @@
45+
static constexpr auto DEADLINE_MS_HEADER = "lambda-runtime-deadline-ms";
46+
static constexpr auto FUNCTION_ARN_HEADER = "lambda-runtime-invoked-function-arn";
47+
static constexpr auto TENANT_ID_HEADER = "lambda-runtime-aws-tenant-id";
48+
+static constexpr auto INVOCATION_ID_HEADER = "lambda-runtime-invocation-id";
49+
50+
enum Endpoints {
51+
INIT,
52+
@@ -294,6 +295,10 @@
53+
req.tenant_id = resp.get_header(TENANT_ID_HEADER);
54+
}
55+
56+
+ if (resp.has_header(INVOCATION_ID_HEADER)) {
57+
+ req.invocation_id = resp.get_header(INVOCATION_ID_HEADER);
58+
+ }
59+
+
60+
if (resp.has_header(DEADLINE_MS_HEADER)) {
61+
auto const& deadline_string = resp.get_header(DEADLINE_MS_HEADER);
62+
constexpr int base = 10;
63+
@@ -310,29 +315,30 @@
64+
return next_outcome(req);
65+
}
66+
67+
-runtime::post_outcome runtime::post_success(std::string const& request_id, invocation_response const& handler_response)
68+
+runtime::post_outcome runtime::post_success(std::string const& request_id, invocation_response const& handler_response, std::string const& invocation_id)
69+
{
70+
std::string const url = m_endpoints[Endpoints::RESULT] + request_id + "/response";
71+
- return do_post(url, handler_response.get_content_type(), handler_response.get_payload(), handler_response.get_xray_response());
72+
+ return do_post(url, handler_response.get_content_type(), handler_response.get_payload(), handler_response.get_xray_response(), invocation_id);
73+
}
74+
75+
-runtime::post_outcome runtime::post_failure(std::string const& request_id, invocation_response const& handler_response)
76+
+runtime::post_outcome runtime::post_failure(std::string const& request_id, invocation_response const& handler_response, std::string const& invocation_id)
77+
{
78+
std::string const url = m_endpoints[Endpoints::RESULT] + request_id + "/error";
79+
- return do_post(url, handler_response.get_content_type(), handler_response.get_payload(), handler_response.get_xray_response());
80+
+ return do_post(url, handler_response.get_content_type(), handler_response.get_payload(), handler_response.get_xray_response(), invocation_id);
81+
}
82+
83+
runtime::post_outcome runtime::post_init_error(runtime_response const& init_error_response)
84+
{
85+
std::string const url = m_endpoints[Endpoints::INIT];
86+
- return do_post(url, init_error_response.get_content_type(), init_error_response.get_payload(), init_error_response.get_xray_response());
87+
+ return do_post(url, init_error_response.get_content_type(), init_error_response.get_payload(), init_error_response.get_xray_response(), "");
88+
}
89+
90+
runtime::post_outcome runtime::do_post(
91+
std::string const& url,
92+
std::string const& content_type,
93+
std::string const& payload,
94+
- std::string const& xray_response)
95+
+ std::string const& xray_response,
96+
+ std::string const& invocation_id)
97+
{
98+
set_curl_post_result_options();
99+
curl_easy_setopt(m_curl_handle, CURLOPT_URL, url.c_str());
100+
@@ -351,6 +357,10 @@
101+
headers = curl_slist_append(headers, "transfer-encoding:");
102+
headers = curl_slist_append(headers, m_user_agent_header.c_str());
103+
104+
+ if (!invocation_id.empty()) {
105+
+ headers = curl_slist_append(headers, ("lambda-runtime-invocation-id: " + invocation_id).c_str());
106+
+ }
107+
+
108+
logging::log_debug(
109+
LOG_TAG, "calculating content length... %s", ("content-length: " + std::to_string(payload.length())).c_str());
110+
headers = curl_slist_append(headers, ("content-length: " + std::to_string(payload.length())).c_str());
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
--- a/CMakeLists.txt
2+
+++ b/CMakeLists.txt
3+
@@ -11,10 +11,17 @@
4+
add_library(${PROJECT_NAME}
5+
"src/logging.cpp"
6+
"src/runtime.cpp"
7+
- "src/backward.cpp"
8+
"${CMAKE_CURRENT_BINARY_DIR}/version.cpp"
9+
)
10+
11+
+include(CheckIncludeFileCXX)
12+
+check_include_file_cxx("execinfo.h" HAVE_EXECINFO_H)
13+
+if (HAVE_EXECINFO_H)
14+
+ target_sources(${PROJECT_NAME} PRIVATE "src/backward.cpp")
15+
+else()
16+
+ message("-- execinfo.h not found, stack traces disabled (musl/Alpine Linux)")
17+
+endif()
18+
+
19+
set_target_properties(${PROJECT_NAME} PROPERTIES
20+
SOVERSION 0
21+
VERSION ${PROJECT_VERSION})

scripts/update_deps.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@ wget -c https://github.com/awslabs/aws-lambda-cpp/archive/v$AWS_LAMBDA_CPP_RELEA
2929
patch -p1 < ../patches/aws-lambda-cpp-add-xray-response.patch && \
3030
patch -p1 < ../patches/aws-lambda-cpp-posting-init-errors.patch && \
3131
patch -p1 < ../patches/aws-lambda-cpp-make-the-runtime-client-user-agent-overrideable.patch && \
32+
patch -p1 < ../patches/aws-lambda-cpp-musl-no-execinfo.patch && \
3233
patch -p1 < ../patches/aws-lambda-cpp-make-lto-optional.patch && \
3334
patch -p1 < ../patches/aws-lambda-cpp-add-content-type.patch && \
3435
patch -p1 < ../patches/aws-lambda-cpp-add-tenant-id.patch && \
35-
patch -p1 < ../patches/aws-lambda-cpp-logging-error.patch
36+
patch -p1 < ../patches/aws-lambda-cpp-logging-error.patch && \
37+
patch -p1 < ../patches/aws-lambda-cpp-add-invocation-id.patch
3638
)
3739

3840
## Pack again and remove the folder

tests/test_bootstrap.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ def test_handle_event_request_happy_case(self):
9595
"invoke_id",
9696
'{"input": "event_body", "aws_request_id": "invoke_id"}',
9797
"application/json",
98+
None,
9899
)
99100

100101
def test_handle_event_request_invalid_client_context(self):
@@ -862,7 +863,7 @@ def test_application_json(self):
862863
)
863864

864865
self.lambda_runtime.post_invocation_result.assert_called_once_with(
865-
"invoke-id", '{"response": "foo"}', "application/json"
866+
"invoke-id", '{"response": "foo"}', "application/json", None
866867
)
867868

868869
def test_binary_request_binary_response(self):
@@ -882,7 +883,7 @@ def test_binary_request_binary_response(self):
882883
)
883884

884885
self.lambda_runtime.post_invocation_result.assert_called_once_with(
885-
"invoke-id", event_body, "application/unknown"
886+
"invoke-id", event_body, "application/unknown", None
886887
)
887888

888889
def test_json_request_binary_response(self):
@@ -902,7 +903,7 @@ def test_json_request_binary_response(self):
902903
)
903904

904905
self.lambda_runtime.post_invocation_result.assert_called_once_with(
905-
"invoke-id", binary_data, "application/unknown"
906+
"invoke-id", binary_data, "application/unknown", None
906907
)
907908

908909
def test_binary_with_application_json(self):
@@ -927,6 +928,7 @@ def test_binary_with_application_json(self):
927928
invoke_id,
928929
error_result,
929930
xray_fault,
931+
invocation_id,
930932
), _ = self.lambda_runtime.post_invocation_error.call_args
931933
error_dict = json.loads(error_result)
932934

0 commit comments

Comments
 (0)