diff --git a/.github/workflows/dockerized-test.yml b/.github/workflows/dockerized-test.yml
new file mode 100644
index 000000000..d83490589
--- /dev/null
+++ b/.github/workflows/dockerized-test.yml
@@ -0,0 +1,68 @@
+name: dockerized-test
+
+permissions:
+ contents: read
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: ['*']
+ paths:
+ - 'aws-lambda-java-core/**'
+ - 'aws-lambda-java-serialization/**'
+ - 'aws-lambda-java-runtime-interface-client/**'
+ - '.github/workflows/dockerized-test.yml'
+ workflow_dispatch:
+
+jobs:
+ dockerized-test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+
+ - name: Set up JDK
+ uses: actions/setup-java@v5
+ with:
+ # The core/serialization/RIC builds require a JDK 8 toolchain
+ # (maven-toolchains-plugin: [1.8,9)); 25 is the JVM Maven runs on.
+ java-version: |
+ 8
+ 25
+ distribution: corretto
+ cache: maven
+
+ - name: Build core, serialization and the RIC under test
+ run: |
+ mvn -B -q install -DskipTests --file aws-lambda-java-core/pom.xml
+ mvn -B -q install -DskipTests --file aws-lambda-java-serialization/pom.xml
+ mvn -B -q clean install -DskipTests -DmultiArch=false \
+ --file aws-lambda-java-runtime-interface-client/pom.xml
+ # Stage core + serialization next to the RIC jar so the image build
+ # can pick all three up from a single ./target folder.
+ cp aws-lambda-java-core/target/aws-lambda-java-core-*.jar \
+ aws-lambda-java-runtime-interface-client/target/
+ cp aws-lambda-java-serialization/target/aws-lambda-java-serialization-*.jar \
+ aws-lambda-java-runtime-interface-client/target/
+
+ - name: Compile the W3C task handler
+ working-directory: aws-lambda-java-runtime-interface-client
+ run: |
+ javac --release 8 -cp "$(ls target/aws-lambda-java-core-*.jar)" \
+ -d test/dockerized/tasks \
+ test/dockerized/handler/W3CHandler.java
+
+ - name: Build the test image
+ working-directory: aws-lambda-java-runtime-interface-client
+ run: |
+ docker build . \
+ -t local/test \
+ -f Dockerfile.test \
+ --build-arg BASE_IMAGE=public.ecr.aws/lambda/java:25
+
+ - name: Run dockerized suites
+ uses: aws/containerized-test-runner-for-aws-lambda@0863dd17b5fc19585250a2405c0f939a77b4f397 # main
+ with:
+ suiteFileArray: '["./aws-lambda-java-runtime-interface-client/test/dockerized/suites/*.json"]'
+ dockerImageName: 'local/test'
+ taskFolder: './aws-lambda-java-runtime-interface-client/test/dockerized/tasks'
diff --git a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Context.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Context.java
index ed9311a11..e9bea4ce7 100644
--- a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Context.java
+++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Context.java
@@ -2,6 +2,9 @@
package com.amazonaws.services.lambda.runtime;
+import java.util.Collections;
+import java.util.Map;
+
/**
*
* The context object allows you to access useful information available within
@@ -119,4 +122,20 @@ default String getTenantId() {
default String getXrayTraceId() {
return null;
}
+
+ /**
+ * Returns the W3C trace-context fields associated with the request at invoke
+ * time.
+ *
+ * The returned map only ever contains the allowlisted W3C trace-context
+ * keys ({@code traceparent}, {@code tracestate}, {@code baggage}) that were
+ * carried on the invoke's client context. Any other key, and any allowlisted
+ * key whose value is not a string, is dropped.
+ *
+ *
+ * @return an unmodifiable map of W3C trace-context fields; empty by default
+ */
+ default Map w3c() {
+ return Collections.emptyMap();
+ }
}
diff --git a/aws-lambda-java-runtime-interface-client/Dockerfile.test b/aws-lambda-java-runtime-interface-client/Dockerfile.test
new file mode 100644
index 000000000..deda8afb4
--- /dev/null
+++ b/aws-lambda-java-runtime-interface-client/Dockerfile.test
@@ -0,0 +1,16 @@
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+ARG BASE_IMAGE=public.ecr.aws/lambda/java:25
+FROM $BASE_IMAGE
+
+COPY target/aws-lambda-java-runtime-interface-client-*.jar /opt/ric/
+COPY target/aws-lambda-java-core-*.jar /opt/ric/
+COPY target/aws-lambda-java-serialization-*.jar /opt/ric/
+
+ENTRYPOINT ["/bin/sh", "-c", \
+"exec /usr/local/bin/aws-lambda-rie java \
+-cp \"/opt/ric/*:/var/task\" \
+com.amazonaws.services.lambda.runtime.api.client.AWSLambda \"$@\"", \
+"sh"]
+CMD ["W3CHandler::getW3c"]
diff --git a/aws-lambda-java-runtime-interface-client/pom.xml b/aws-lambda-java-runtime-interface-client/pom.xml
index d84aa51d8..11090062c 100644
--- a/aws-lambda-java-runtime-interface-client/pom.xml
+++ b/aws-lambda-java-runtime-interface-client/pom.xml
@@ -65,7 +65,7 @@
com.amazonaws
aws-lambda-java-core
- 1.4.0
+ 1.4.1-SNAPSHOT
com.amazonaws
diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaClientContext.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaClientContext.java
index 3baa5347b..d070cf3d1 100644
--- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaClientContext.java
+++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaClientContext.java
@@ -14,6 +14,7 @@ public class LambdaClientContext implements ClientContext {
private LambdaClientContextClient client;
private Map custom;
private Map env;
+ private Object w3c;
public Client getClient() {
return client;
@@ -26,4 +27,10 @@ public Map getCustom() {
public Map getEnvironment() {
return env;
}
+
+ Object readAndStripW3c() {
+ Object raw = this.w3c;
+ this.w3c = null;
+ return raw;
+ }
}
diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContext.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContext.java
index 20b77262d..de3649a99 100644
--- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContext.java
+++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContext.java
@@ -9,9 +9,22 @@
import com.amazonaws.services.lambda.runtime.CognitoIdentity;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.LambdaLogger;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
public class LambdaContext implements Context {
+ /**
+ * Allowlist of W3C trace-context fields that may be surfaced through
+ * {@link #w3c()}. Any other key carried on {@code clientContext.w3c} is
+ * ignored, and any allowlisted key whose value is not a string is dropped.
+ */
+ public static final List W3C_ALLOWED_FIELDS =
+ Collections.unmodifiableList(Arrays.asList("traceparent", "tracestate", "baggage"));
+
private int memoryLimit;
private final String awsRequestId;
private final String logGroupName;
@@ -25,6 +38,7 @@ public class LambdaContext implements Context {
private final String tenantId;
private final String xrayTraceId;
private final LambdaLogger logger;
+ private final Map w3cFields;
public LambdaContext(
int memoryLimit,
@@ -52,6 +66,7 @@ public LambdaContext(
this.invokedFunctionArn = invokedFunctionArn;
this.tenantId = tenantId;
this.xrayTraceId = xrayTraceId;
+ this.w3cFields = extractAndStripW3c(clientContext);
this.logger = com.amazonaws.services.lambda.runtime.LambdaRuntime.getLogger();
}
@@ -105,6 +120,32 @@ public String getXrayTraceId() {
return xrayTraceId;
}
+ @Override
+ public Map w3c() {
+ return w3cFields;
+ }
+
+ private static Map extractAndStripW3c(ClientContext clientContext) {
+ if (!(clientContext instanceof LambdaClientContext)) {
+ return Collections.emptyMap();
+ }
+
+ Object rawW3c = ((LambdaClientContext) clientContext).readAndStripW3c();
+ if (!(rawW3c instanceof Map)) {
+ return Collections.emptyMap();
+ }
+
+ Map, ?> source = (Map, ?>) rawW3c;
+ Map fields = new LinkedHashMap<>();
+ for (String key : W3C_ALLOWED_FIELDS) {
+ Object value = source.get(key);
+ if (value instanceof String) {
+ fields.put(key, (String) value);
+ }
+ }
+ return Collections.unmodifiableMap(fields);
+ }
+
public LambdaLogger getLogger() {
return logger;
}
diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContextTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContextTest.java
index f7da76198..167774ad9 100644
--- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContextTest.java
+++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContextTest.java
@@ -2,9 +2,17 @@
package com.amazonaws.services.lambda.runtime.api.client.api;
+import com.amazonaws.services.lambda.runtime.ClientContext;
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer;
+import com.amazonaws.services.lambda.runtime.serialization.factories.GsonFactory;
import org.junit.jupiter.api.Test;
+import java.util.Map;
+
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class LambdaContextTest {
@@ -58,4 +66,97 @@ private LambdaContext createContextWithDeadline(long deadlineTimeInMs) {
return new LambdaContext(MEMORY_LIMIT, deadlineTimeInMs, REQUEST_ID, LOG_GROUP_NAME, LOG_STREAM_NAME,
FUNCTION_NAME, IDENTITY, FUNCTION_VERSION, INVOKED_FUNCTION_ARN, TENANT_ID, X_RAY_TRACE_ID, CLIENT_CONTEXT);
}
+
+ private LambdaContext w3cContext(ClientContext clientContext) {
+ return new LambdaContext(MEMORY_LIMIT, System.currentTimeMillis() + 1000, REQUEST_ID, LOG_GROUP_NAME,
+ LOG_STREAM_NAME, FUNCTION_NAME, IDENTITY, FUNCTION_VERSION, INVOKED_FUNCTION_ARN, TENANT_ID,
+ X_RAY_TRACE_ID, clientContext);
+ }
+
+ private LambdaClientContext clientContextFromJson(String json) {
+ PojoSerializer serializer =
+ GsonFactory.getInstance().getSerializer(LambdaClientContext.class);
+ return serializer.fromJson(json);
+ }
+
+ @Test
+ public void w3c_returnsEmptyWhenClientContextIsNull() {
+ assertTrue(w3cContext(null).w3c().isEmpty());
+ }
+
+ @Test
+ public void w3c_returnsEmptyWhenNoW3cKey() {
+ LambdaClientContext cc = clientContextFromJson("{\"custom\": {\"value\": \"test\"}}");
+ assertTrue(w3cContext(cc).w3c().isEmpty());
+ }
+
+ @Test
+ public void w3c_returnsAllAllowlistedFields() {
+ LambdaClientContext cc = clientContextFromJson(
+ "{\"w3c\": {" +
+ "\"traceparent\": \"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01\"," +
+ "\"tracestate\": \"rojo=00f067aa0ba902b7\"," +
+ "\"baggage\": \"userId=alice\"}}");
+
+ Map w3c = w3cContext(cc).w3c();
+
+ assertEquals(3, w3c.size());
+ assertEquals("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01", w3c.get("traceparent"));
+ assertEquals("rojo=00f067aa0ba902b7", w3c.get("tracestate"));
+ assertEquals("userId=alice", w3c.get("baggage"));
+ }
+
+ @Test
+ public void w3c_dropsNonAllowlistedKeysAndNonStringValues() {
+ LambdaClientContext cc = clientContextFromJson(
+ "{\"w3c\": {" +
+ "\"baggage\": \"keep=me\"," +
+ "\"unknownField\": \"drop-me\"," +
+ "\"traceparent\": 42," +
+ "\"tracestate\": null}}");
+
+ Map w3c = w3cContext(cc).w3c();
+
+ assertEquals(1, w3c.size());
+ assertEquals("keep=me", w3c.get("baggage"));
+ }
+
+ @Test
+ public void w3c_treatsNonObjectPayloadAsEmpty() {
+ assertTrue(w3cContext(clientContextFromJson("{\"w3c\": \"not-an-object\"}")).w3c().isEmpty());
+ assertTrue(w3cContext(clientContextFromJson("{\"w3c\": [\"baggage=abc\"]}")).w3c().isEmpty());
+ }
+
+ @Test
+ public void w3c_isStrippedFromClientContextAndIsUnmodifiable() {
+ LambdaClientContext cc = clientContextFromJson(
+ "{\"custom\": {\"value\": \"test\"}, \"w3c\": {\"baggage\": \"userId=alice\"}}");
+
+ LambdaContext context = w3cContext(cc);
+
+ assertEquals("userId=alice", context.w3c().get("baggage"));
+ assertThrows(UnsupportedOperationException.class, () -> context.w3c().put("x", "y"));
+ assertEquals("test", cc.getCustom().get("value"));
+ assertTrue(w3cContext(cc).w3c().isEmpty());
+ }
+
+ @Test
+ public void w3c_defaultsToEmptyOnBareContextInterface() {
+ Context bare = new Context() {
+ public String getAwsRequestId() { return null; }
+ public String getLogGroupName() { return null; }
+ public String getLogStreamName() { return null; }
+ public String getFunctionName() { return null; }
+ public String getFunctionVersion() { return null; }
+ public String getInvokedFunctionArn() { return null; }
+ public com.amazonaws.services.lambda.runtime.CognitoIdentity getIdentity() { return null; }
+ public ClientContext getClientContext() { return null; }
+ public int getRemainingTimeInMillis() { return 0; }
+ public int getMemoryLimitInMB() { return 0; }
+ public com.amazonaws.services.lambda.runtime.LambdaLogger getLogger() { return null; }
+ };
+
+ assertTrue(bare.w3c().isEmpty());
+ assertFalse(bare.w3c() == null);
+ }
}
diff --git a/aws-lambda-java-runtime-interface-client/test/dockerized/handler/W3CHandler.java b/aws-lambda-java-runtime-interface-client/test/dockerized/handler/W3CHandler.java
new file mode 100644
index 000000000..d2a7ba3e6
--- /dev/null
+++ b/aws-lambda-java-runtime-interface-client/test/dockerized/handler/W3CHandler.java
@@ -0,0 +1,26 @@
+/*
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+import com.amazonaws.services.lambda.runtime.ClientContext;
+import com.amazonaws.services.lambda.runtime.Context;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+
+public class W3CHandler {
+
+ public Map getW3c(Map event, Context context) {
+ return context.w3c();
+ }
+
+ public Map getW3cAndCustom(Map event, Context context) {
+ Map result = new LinkedHashMap<>();
+ result.put("w3c", context.w3c());
+ ClientContext clientContext = context.getClientContext();
+ result.put("custom", clientContext == null ? null : clientContext.getCustom());
+ return result;
+ }
+}
diff --git a/aws-lambda-java-runtime-interface-client/test/dockerized/suites/w3c.json b/aws-lambda-java-runtime-interface-client/test/dockerized/suites/w3c.json
new file mode 100644
index 000000000..a5c03991a
--- /dev/null
+++ b/aws-lambda-java-runtime-interface-client/test/dockerized/suites/w3c.json
@@ -0,0 +1,111 @@
+{
+ "tests": [
+ {
+ "name": "w3c_returns_empty_when_no_client_context_header",
+ "handler": "W3CHandler::getW3c",
+ "request": {},
+ "assertions": [
+ { "response": {} }
+ ]
+ },
+
+ {
+ "name": "w3c_returns_empty_when_client_context_has_no_w3c_key",
+ "handler": "W3CHandler::getW3c",
+ "request": {},
+ "clientContext": {
+ "custom": { "value": "test" }
+ },
+ "assertions": [
+ { "response": {} }
+ ]
+ },
+
+ {
+ "name": "w3c_returns_baggage_only",
+ "handler": "W3CHandler::getW3c",
+ "request": {},
+ "clientContext": {
+ "w3c": { "baggage": "userId=alice" }
+ },
+ "assertions": [
+ { "response": { "baggage": "userId=alice" } }
+ ]
+ },
+
+ {
+ "name": "w3c_returns_all_three_allowlisted_fields",
+ "handler": "W3CHandler::getW3c",
+ "request": {},
+ "clientContext": {
+ "w3c": {
+ "traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
+ "tracestate": "rojo=00f067aa0ba902b7",
+ "baggage": "userId=alice"
+ }
+ },
+ "assertions": [
+ {
+ "response": {
+ "traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
+ "tracestate": "rojo=00f067aa0ba902b7",
+ "baggage": "userId=alice"
+ }
+ }
+ ]
+ },
+
+ {
+ "name": "w3c_drops_non_allowlisted_keys_and_non_string_values",
+ "handler": "W3CHandler::getW3c",
+ "request": {},
+ "clientContext": {
+ "w3c": {
+ "baggage": "keep=me",
+ "unknownField": "should-not-appear",
+ "traceparent": 42,
+ "tracestate": null
+ }
+ },
+ "assertions": [
+ { "response": { "baggage": "keep=me" } }
+ ]
+ },
+
+ {
+ "name": "w3c_treats_non_object_payload_as_empty",
+ "handler": "W3CHandler::getW3c",
+ "request": {},
+ "clientContext": {
+ "w3c": "not-an-object"
+ },
+ "assertions": [
+ { "response": {} }
+ ]
+ },
+
+ {
+ "name": "w3c_coexists_with_client_context_custom",
+ "handler": "W3CHandler::getW3cAndCustom",
+ "request": {},
+ "clientContext": {
+ "custom": { "value": "test" },
+ "w3c": {
+ "traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
+ "baggage": "userId=alice"
+ }
+ },
+ "assertions": [
+ {
+ "response": {
+ "w3c": {
+ "traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
+ "baggage": "userId=alice"
+ },
+ "custom": { "value": "test" }
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/aws-lambda-java-runtime-interface-client/test/dockerized/tasks/.gitignore b/aws-lambda-java-runtime-interface-client/test/dockerized/tasks/.gitignore
new file mode 100644
index 000000000..c96a04f00
--- /dev/null
+++ b/aws-lambda-java-runtime-interface-client/test/dockerized/tasks/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
\ No newline at end of file