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
18 changes: 15 additions & 3 deletions include/aws/lambda-runtime/runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ struct invocation_request {
*/
std::string tenant_id;

/**
* The unique invocation ID for cross-wiring protection.
*/
std::string invocation_id;

/**
* The number of milliseconds left before lambda terminates the current execution.
*/
Expand Down Expand Up @@ -199,12 +204,18 @@ class runtime {
/**
* 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 = "");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't use default argument values. Instead, add an overload and make this one call it with a default value.
Parameters with default values make it hard to change this API without breaking backwards compatibility.

@marcomagdy marcomagdy Jul 23, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment goes for post_success

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for your input, you're right, let's do that


/**
* Tells lambda that the runtime has failed during initialization.
Expand All @@ -218,7 +229,8 @@ class runtime {
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 = "");
std::string const m_user_agent_header;
std::array<std::string const, 3> const m_endpoints;
};
Expand Down
39 changes: 32 additions & 7 deletions src/runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ static constexpr auto COGNITO_IDENTITY_HEADER = "lambda-runtime-cognito-identity
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";
struct curl_handle_wrapper {
CURL* handle;
curl_handle_wrapper() : handle(curl_easy_init()) {}
Expand Down Expand Up @@ -318,6 +319,11 @@ runtime::next_outcome runtime::get_next()
req.tenant_id = std::move(out).get_result();
}

out = resp.get_header(INVOCATION_ID_HEADER);
if (out.is_success()) {
req.invocation_id = std::move(out).get_result();
}

out = resp.get_header(DEADLINE_MS_HEADER);
if (out.is_success()) {
auto const& deadline_string = std::move(out).get_result();
Expand All @@ -335,18 +341,32 @@ runtime::next_outcome runtime::get_next()
return {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());
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());
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)
Expand All @@ -363,7 +383,8 @@ 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(lambda_runtime::m_curl_handle, CURLOPT_URL, url.c_str());
Expand All @@ -382,6 +403,10 @@ runtime::post_outcome runtime::do_post(
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, (std::string(INVOCATION_ID_HEADER) + ": " + 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());
Expand Down Expand Up @@ -479,13 +504,13 @@ void run_handler(std::function<invocation_response(invocation_request const&)> c
logging::log_info(LOG_TAG, "Invoking user handler completed.");

if (res.is_success()) {
const auto post_outcome = rt.post_success(req.request_id, res);
const auto post_outcome = rt.post_success(req.request_id, res, req.invocation_id);
if (!handle_post_outcome(post_outcome, req.request_id)) {
return; // TODO: implement a better retry strategy
}
}
else {
const auto post_outcome = rt.post_failure(req.request_id, res);
const auto post_outcome = rt.post_failure(req.request_id, res, req.invocation_id);
if (!handle_post_outcome(post_outcome, req.request_id)) {
return; // TODO: implement a better retry strategy
}
Expand Down
17 changes: 17 additions & 0 deletions tests/unit/thread_local_curl_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,20 @@ TEST(ThreadLocalCurl, sequential_requests_on_same_runtime)
ASSERT_FALSE(outcome.is_success());
}
}

TEST(ThreadLocalCurl, post_success_with_invocation_id_does_not_crash)
{
runtime rt("http://127.0.0.1:9001");
auto response = invocation_response::success("test payload", "application/json");
auto outcome = rt.post_success("test-request-id", response, "test-invocation-uuid-1234");
// Connection will fail but the invocation_id header path should not crash
ASSERT_FALSE(outcome.is_success());
}

TEST(ThreadLocalCurl, post_failure_with_invocation_id_does_not_crash)
{
runtime rt("http://127.0.0.1:9001");
auto response = invocation_response::failure("error msg", "TestError", "");
auto outcome = rt.post_failure("test-request-id", response, "test-invocation-uuid-5678");
ASSERT_FALSE(outcome.is_success());
}
9 changes: 9 additions & 0 deletions tests/unit/unit_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ TEST(InvocationRequestTest, default_fields_are_empty)
EXPECT_TRUE(req.cognito_identity.empty());
EXPECT_TRUE(req.function_arn.empty());
EXPECT_TRUE(req.tenant_id.empty());
EXPECT_TRUE(req.invocation_id.empty());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we expect it to be empty? or does it reflect the missing req.invocation_id being passed in run_handler?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test asserts the default-constructed state of invocation_request (all fields empty before get_next() populates them)

}

// --- version tests (no AWS SDK needed) ---
Expand All @@ -353,3 +354,11 @@ TEST(VersionTest, version_format)
}
EXPECT_EQ(2, dots);
}

// --- invocation_id cross-wiring protection tests ---

TEST(InvocationRequestTest, invocation_id_default_empty)
{
invocation_request req;
EXPECT_TRUE(req.invocation_id.empty());
}
Loading