Skip to content
Merged
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
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ python3 tests/test_native_image_script.py
```

These tests stub Maven and Docker to check packaging and failure handling.
They are not run by Maven or the current CI workflow.
They are not run by Maven. The source-build CI workflow runs them separately from the real native smoke.

### Test with Main

Expand Down Expand Up @@ -127,7 +127,7 @@ software.amazon.awssdk.services.lambda.model.ResourceNotFoundException: Function

This source line uses Jackson 3.2.3 and json-schema-validator 3.0.8. Build the Jackson 3 light-4j, http-client, REST and AWS Lambda prerequisites first; older snapshot binaries are not compatible. Jackson annotations retain their com.fasterxml.jackson.annotation package.

The runtime mapper uses Jackson2Defaults with its existing permissive unknown-property override. Mapper instances are immutable. Java time and Optional support are built in. Classic SnakeYAML compatibility pins are retained until the stack-wide cleanup.
The runtime uses the event-only `LambdaRuntimeMappers.mapper()` API from custom-runtime, built from `JacksonDefaults` with its permissive unknown-property override. Mapper instances are immutable. Java time and Optional support are built in. Classic SnakeYAML compatibility pins are retained until the stack-wide cleanup.

Local qualification includes the full JVM suite, sanitized event/validation parity against the Jackson 2 baseline, and an offline GraalVM 25 native-image build. A native-image build does not establish deployed AWS behavior or complete reflection coverage for every middleware/configuration. The separate conformance worktree is deferred.

Expand All @@ -138,7 +138,11 @@ MAVEN_REPO_LOCAL=/path/to/isolated/m2 bash native-image-linux.sh
python3 tests/native_runtime_smoke.py target/custom-runtime/lambda-native
```

The smoke check binds an API Gateway event, runs the local health handler, and verifies success and builder-based error serialization. It uses no AWS endpoint or credentials. `-H:+AddAllCharsets` is required by SnakeYAML Engine's UTF-32 input detection. The check does not qualify every middleware chain or deployed AWS behavior.
The smoke check binds API Gateway events with an unknown future field and runs specification, validator and health handlers. It checks valid referenced query/header/body schemas, invalid query/header/body values, an unknown path, a method mismatch, and builder-based error serialization. The OpenAPI document and handler/validator settings are controlled fixtures. The status catalog and injection settings are copied from the packaged application's `config/status.yml` and `config/config.yml`; their contents are not duplicated in the fixture. By default the script requires exactly one shaded `target/lambda-native-*.jar` (excluding sources and Javadoc JARs); use `--application-jar /path/to/runtime.jar` to select one explicitly.

It uses no AWS endpoint or credentials. `-H:+AddAllCharsets` is required by SnakeYAML Engine's UTF-32 input detection. The check does not qualify every middleware chain, all default-resource packaging, or deployed AWS behavior.

The native reflection file currently includes version-specific JSON overlay sideband accessors (`MapOverlay$1.getOverlay()` and `ListOverlay$1.getOverlay()`) and the OpenAPI path cache's Caffeine factories (`SSMS.FACTORY` and `PSMS()`), covered by this smoke. These remain application registrations until their owning libraries ship equivalent native metadata. Migrate native consumers together and remove these entries only after updated source pins and native smoke results establish equivalent coverage; moving them is separate from this mapper change.

### Reproducible Jackson 3 source build

Expand Down
2 changes: 1 addition & 1 deletion ci/jackson3-source-refs.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"http-client": "5ecee29dbb08d813512f5379c2e1b902d553c5d4",
"json-overlay": "e309fd63f95317067c9b13a4bed7ac16727bfea5",
"light-4j": "97b2359b92ed3e1d05cac04f2da8760aa954e49d",
"light-aws-lambda": "dddbbc9e7f6f690f8688ceac024728b6a84f4808",
"light-aws-lambda": "5913c7c25b01b6d384fd9c39014dc91d63dfc35e",
"light-rest-4j": "a31955945036d9108b7e9bb72167415cc9cfc287",
"openapi-parser": "9ae849056e49f7c796fe28bfe6093fbe4847cc5c",
"yaml-rule": "c237cc3a25720f43e57bf571e25d5fa06687aa3b"
Expand Down
16 changes: 16 additions & 0 deletions reflect.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
[
{
"name": "com.github.benmanes.caffeine.cache.SSMS",
"fields": [{ "name": "FACTORY" }]
},
{
"name": "com.github.benmanes.caffeine.cache.PSMS",
"methods": [{ "name": "<init>", "parameterTypes": [] }]
},
{
"name": "com.networknt.jsonoverlay.MapOverlay$1",
"methods": [{ "name": "getOverlay", "parameterTypes": [] }]
},
{
"name": "com.networknt.jsonoverlay.ListOverlay$1",
"methods": [{ "name": "getOverlay", "parameterTypes": [] }]
},
{
"name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent",
"methods": [
Expand Down
5 changes: 1 addition & 4 deletions src/main/java/com/networknt/aws/lambda/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@

import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.ObjectMapper;
import com.networknt.jackson.JacksonDefaults;
import com.networknt.aws.lambda.app.LambdaApp;

import java.io.IOException;


public class Main {
private static final ObjectMapper OBJECT_MAPPER = JacksonDefaults.builder()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build();
private static final ObjectMapper OBJECT_MAPPER = LambdaRuntimeMappers.mapper();

public static void main(String[] args) throws IOException {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,14 @@
import org.junit.jupiter.api.Test;
import tools.jackson.databind.ObjectMapper;

import java.lang.reflect.Field;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;

class Jackson3NativeCompatibilityTest {
private ObjectMapper mainMapper() throws Exception {
Field field = Main.class.getDeclaredField("OBJECT_MAPPER");
field.setAccessible(true);
return (ObjectMapper) field.get(null);
}

@Test
void runtimeMapperKeepsPermissiveEventBinding() throws Exception {
APIGatewayProxyRequestEvent event = mainMapper().readValue(
APIGatewayProxyRequestEvent event = LambdaRuntimeMappers.mapper().readValue(
"{\"path\":\"/v1/pets\",\"httpMethod\":\"POST\",\"futureField\":true} trailing",
APIGatewayProxyRequestEvent.class);
assertEquals("/v1/pets", event.getPath());
Expand All @@ -27,7 +21,7 @@ void runtimeMapperKeepsPermissiveEventBinding() throws Exception {

@Test
void responseRetainsJackson2ShapeAndRoundTrips() throws Exception {
ObjectMapper mapper = mainMapper();
ObjectMapper mapper = LambdaRuntimeMappers.mapper();
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent()
.withStatusCode(200).withBody("{\"ok\":true}").withIsBase64Encoded(false);
String json = mapper.writeValueAsString(response);
Expand All @@ -39,6 +33,6 @@ void responseRetainsJackson2ShapeAndRoundTrips() throws Exception {
@Test
void runtimeMapperRetainsJackson2StreamDepth() throws Exception {
String nested = "[".repeat(600) + "0" + "]".repeat(600);
assertDoesNotThrow(() -> mainMapper().readTree(nested));
assertDoesNotThrow(() -> LambdaRuntimeMappers.mapper().readTree(nested));
}
}
5 changes: 1 addition & 4 deletions src/test/java/com/networknt/aws/lambda/TestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.ObjectMapper;
import com.networknt.jackson.JacksonDefaults;

public class TestUtils {

Expand Down Expand Up @@ -133,8 +131,7 @@ public class TestUtils {
" }\n" +
"}";

private static final ObjectMapper OBJECT_MAPPER = JacksonDefaults.builder()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build();
private static final ObjectMapper OBJECT_MAPPER = LambdaRuntimeMappers.mapper();

public static APIGatewayProxyRequestEvent createTestRequestEvent() {
APIGatewayProxyRequestEvent apiGatewayProxyRequestEvent;
Expand Down
101 changes: 93 additions & 8 deletions tests/native_runtime_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@
import subprocess
import tempfile
import threading
import zipfile


def invoke(binary, handler, expected_endpoint):
def invoke(binary, status_catalog, handler, expected_endpoint, event, *, config_catalog):
received = queue.Queue()
event = {"path": "/health", "httpMethod": "GET", "body": None,
"isBase64Encoded": False, "headers": {}, "multiValueHeaders": {},
"requestContext": {"requestId": "native-smoke", "identity": {"sourceIp": "127.0.0.1"}}}

class RuntimeApi(http.server.BaseHTTPRequestHandler):
def log_message(self, *args):
Expand All @@ -39,19 +37,73 @@ def do_POST(self):

with tempfile.TemporaryDirectory(prefix="native-runtime-smoke-") as directory:
config = Path(directory)
(config / "config.yml").write_bytes(config_catalog)
(config / "handler.yml").write_text("""enabled: true
handlers:
- com.networknt.aws.lambda.handler.middleware.specification.OpenApiMiddleware@spec
- com.networknt.aws.lambda.handler.middleware.validator.ValidatorMiddleware@validator
- com.networknt.aws.lambda.handler.health.HealthCheckHandler@health
chains: {}
paths:
- path: /health
method: get
exec: [health]
defaultHandlers: [health]
exec: [spec, validator, health]
defaultHandlers: [spec, validator, health]
""")
(config / "health.yml").write_text("enabled: true\ndownstreamEnabled: false\n")
(config / "lambda-app.yml").write_text("lambdaAppId: native-smoke\nencodeBase64Request: false\nencodeBase64Response: false\n")
(config / "values.yml").write_text("{}\n")
(config / "service.yml").write_text("singletons: []\n")
(config / "status.yml").write_bytes(status_catalog)
# Controlled fixture settings exercise validation; this is not default-config qualification.
(config / "openapi-inject.yml").write_text("{}\n")
(config / "openapi-handler.yml").write_text("enabled: true\nmultipleSpec: false\n")
(config / "openapi-validator.yml").write_text("enabled: true\nskipPathPrefixes: []\n")
(config / "openapi.yaml").write_text("""openapi: 3.0.3
info:
title: Native validation smoke
version: '1'
servers:
- url: /
paths:
/health:
get:
parameters:
- name: count
in: query
required: true
schema:
$ref: '#/components/schemas/Count'
- name: x-count
in: header
required: true
schema:
$ref: '#/components/schemas/Count'
responses:
'200':
description: OK
/body:
post:
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Payload'
responses:
'200':
description: OK
components:
schemas:
Count:
type: integer
Payload:
type: object
required: [value]
properties:
value:
type: integer
""")
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), RuntimeApi)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
Expand Down Expand Up @@ -85,15 +137,48 @@ def do_POST(self):
def main():
parser = argparse.ArgumentParser()
parser.add_argument("binary", type=Path)
parser.add_argument("--application-jar", type=Path,
help="Shaded runtime JAR providing framework config and status catalog (default: target/lambda-native-*.jar)")
args = parser.parse_args()
binary = args.binary.resolve(strict=True)
response = invoke(binary, "com.networknt.aws.lambda.app.LambdaApp", "response")
jars = [args.application_jar] if args.application_jar else [
jar for jar in (Path(__file__).resolve().parents[1] / "target").glob("lambda-native-*.jar")
if not jar.name.endswith(("-sources.jar", "-javadoc.jar"))]
if len(jars) != 1:
parser.error("Expected one shaded runtime JAR; specify --application-jar explicitly")
with zipfile.ZipFile(jars[0]) as application_jar:
status_catalog = application_jar.read("config/status.yml")
config_catalog = application_jar.read("config/config.yml")
event = {"path": "/health", "httpMethod": "GET", "body": None,
"isBase64Encoded": False, "headers": {"x-count": "7"}, "multiValueHeaders": {},
"queryStringParameters": {"count": "7"}, "futureEventField": {"nested": True},
"requestContext": {"requestId": "native-smoke", "identity": {"sourceIp": "127.0.0.1"}}}
handler = "com.networknt.aws.lambda.app.LambdaApp"
response = invoke(binary, status_catalog, handler, "response", event, config_catalog=config_catalog)
assert response["statusCode"] == 200, response
assert response["body"] == "OK", response
assert response["isBase64Encoded"] is False, response
error = invoke(binary, "native.smoke.MissingHandler", "error")
body_event = event | {"path": "/body", "httpMethod": "POST", "body": '{"value":7}',
"headers": {"content-type": "application/json"}, "queryStringParameters": {}}
body_response = invoke(binary, status_catalog, handler, "response", body_event, config_catalog=config_catalog)
assert body_response["statusCode"] == 200, body_response
assert body_response["body"] == "OK", body_response
cases = [
("query", event | {"queryStringParameters": {"count": "invalid"}}, 400, "ERR11004"),
("header", event | {"headers": {"x-count": "invalid"}}, 400, "ERR11004"),
("body", body_event | {"body": '{"value":"invalid"}'}, 400, "ERR11004"),
("unknown path", event | {"path": "/missing"}, 404, "ERR10007"),
("method mismatch", event | {"httpMethod": "DELETE"}, 405, "ERR10008"),
]
for name, invalid_event, status, code in cases:
invalid = invoke(binary, status_catalog, handler, "response", invalid_event, config_catalog=config_catalog)
assert invalid["statusCode"] == status, (name, invalid)
assert json.loads(invalid["body"])["code"] == code, (name, invalid)
error = invoke(binary, status_catalog, "native.smoke.MissingHandler", "error", event, config_catalog=config_catalog)
assert "native.smoke.MissingHandler" in error["message"], error
print("Native Runtime smoke passed: API Gateway binding, health response and builder error serialization")
print("Native Runtime validation passed: unknown event fields and referenced query-schema rejection")
print("Native Runtime validation passed: body and header schemas, unknown path and method mismatch")


if __name__ == "__main__":
Expand Down
Loading