Skip to content

Add direct-style AwsSyncServerInterpreter for AWS Lambda#5149

Open
adamw wants to merge 8 commits intomasterfrom
add-aws-sync-server-interpreter
Open

Add direct-style AwsSyncServerInterpreter for AWS Lambda#5149
adamw wants to merge 8 commits intomasterfrom
add-aws-sync-server-interpreter

Conversation

@adamw
Copy link
Copy Markdown
Member

@adamw adamw commented Mar 27, 2026

Summary

  • Adds AwsSyncServerInterpreter, AwsSyncServerOptions, and SyncLambdaHandler — a direct-style (Identity monad) AWS Lambda interpreter that requires no effect library
  • Placed in lambda-core alongside the existing AwsFutureServerInterpreter, with SyncLambdaHandler in JVM-specific sources
  • Adds aws-lambda-java-runtime-interface-client as a JVM-specific dependency on lambda-core
  • Updates AWS serverless documentation with new interpreter and dependency info

Test plan

  • Compiles on Scala 3 (awsLambdaCore3 / compile)
  • Compiles on Scala 2.13 (awsLambdaCore / compile)
  • CI passes

🤖 Generated with Claude Code

Adds a direct-style (Identity monad) AWS Lambda interpreter alongside
the existing Future and cats-effect interpreters in lambda-core.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 27, 2026 13:26
@adamw
Copy link
Copy Markdown
Member Author

adamw commented Mar 27, 2026

cc @baldram

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new direct-style AWS Lambda server interpreter for Tapir’s AWS serverless integration, enabling JVM Lambda usage without an effect library by using Identity.

Changes:

  • Introduces AwsSyncServerInterpreter and AwsSyncServerOptions (Identity/direct-style).
  • Adds SyncLambdaHandler (JVM RequestStreamHandler) to run Tapir endpoints in AWS Lambda without an effect runtime.
  • Updates AWS serverless docs and adds a JVM-only dependency needed for the new handler.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
serverless/aws/lambda-core/src/main/scalajvm/sttp/tapir/serverless/aws/lambda/SyncLambdaHandler.scala New JVM direct-style Lambda handler that decodes requests and renders AwsResponse.
serverless/aws/lambda-core/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsSyncServerOptions.scala Adds Identity-based server options builder (interceptors + encoding flag).
serverless/aws/lambda-core/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsSyncServerInterpreter.scala Adds Identity-based interpreter wiring + implicit MonadError[Identity].
doc/server/aws.md Documents the new direct-style interpreter/handler and dependency selection.
build.sbt Adds JVM-only AWS Lambda runtime interface client dependency to tapir-aws-lambda-core.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +25 to +27
override def handleRequest(input: InputStream, output: OutputStream, context: Context): Unit = {
val server: AwsSyncServerInterpreter = AwsSyncServerInterpreter(options)

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

handleRequest largely duplicates the request decoding / V1->V2 mapping / response writing logic that already exists in LambdaHandler (cats-effect) and ZioLambdaHandler. With multiple copies, behavior can easily drift over time (e.g., error handling, encoding defaults). Consider extracting the shared JSON decode + request normalization + response rendering into a small helper in lambda-core that the various handlers can reuse.

Copilot uses AI. Check for mistakes.
}

def default[R: Decoder](endpoints: List[ServerEndpoint[Any, Identity]]): SyncLambdaHandler[R] =
apply(endpoints, AwsSyncServerOptions.noEncoding)
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

SyncLambdaHandler.default currently uses AwsSyncServerOptions.noEncoding, which makes the method name misleading (it doesn't return the actual AwsSyncServerOptions.default). Consider either switching to AwsSyncServerOptions.default here, or renaming the method to reflect that it disables encoding (e.g., noEncoding).

Suggested change
apply(endpoints, AwsSyncServerOptions.noEncoding)
apply(endpoints, AwsSyncServerOptions.default)

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +29
val allBytes = input.readAllBytes()
val decoded = decode[R](new String(allBytes, StandardCharsets.UTF_8))
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

InputStream.readAllBytes() is a Java 9+ API. This project still targets JVM 1.8 for Scala 2 (see commonJvmSettings), so calling this can fail at runtime on Java 8 with NoSuchMethodError. Consider replacing this with a Java-8-compatible read loop (e.g., via ByteArrayOutputStream) so the handler remains usable on Java 8 runtimes.

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +31
override def handleRequest(input: InputStream, output: OutputStream, context: Context): Unit = {
val server: AwsSyncServerInterpreter = AwsSyncServerInterpreter(options)

val allBytes = input.readAllBytes()
val decoded = decode[R](new String(allBytes, StandardCharsets.UTF_8))
val response = decoded match {
case Left(e) => AwsResponse.badRequest(s"Invalid AWS request: ${e.getMessage}")
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

This introduces new request/response handling logic in handleRequest (decoding the event, V1->V2 mapping, and writing the JSON response), but there are no tests covering the new SyncLambdaHandler behavior. Consider adding a small JVM unit test that feeds an InputStream with a sample AwsRequest/AwsRequestV1 JSON and asserts on the serialized AwsResponse written to the OutputStream (including the invalid-JSON -> 400 path).

Copilot uses AI. Check for mistakes.
adamw and others added 2 commits March 27, 2026 15:04
Use fully qualified sttp.shared.Identity to avoid shadowing by
case class Identity in the same package. Fix SyncLambdaHandler.default
to use AwsSyncServerOptions.default. Add SyncLambdaApiExample.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +24 to +34
override def handleRequest(input: InputStream, output: OutputStream, context: Context): Unit = {
val server: AwsSyncServerInterpreter = AwsSyncServerInterpreter(options)

val allBytes = input.readAllBytes()
val decoded = decode[R](new String(allBytes, StandardCharsets.UTF_8))
val response = decoded match {
case Left(e) => AwsResponse.badRequest(s"Invalid AWS request: ${e.getMessage}")
case Right(awsRequest) =>
awsRequest match {
case r: AwsRequestV1 => server.toRoute(getAllEndpoints)(r.toV2)
case r: AwsRequest => server.toRoute(getAllEndpoints)(r)
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

server.toRoute(getAllEndpoints) is computed inside request processing (and twice, once per case). toRoute builds a ServerInterpreter, so this adds avoidable overhead on every Lambda invocation. Consider computing the route once (e.g., a lazy val route = AwsSyncServerInterpreter(options).toRoute(getAllEndpoints)) and reusing it for both request types, converting AwsRequestV1 to V2 before calling the route.

Suggested change
override def handleRequest(input: InputStream, output: OutputStream, context: Context): Unit = {
val server: AwsSyncServerInterpreter = AwsSyncServerInterpreter(options)
val allBytes = input.readAllBytes()
val decoded = decode[R](new String(allBytes, StandardCharsets.UTF_8))
val response = decoded match {
case Left(e) => AwsResponse.badRequest(s"Invalid AWS request: ${e.getMessage}")
case Right(awsRequest) =>
awsRequest match {
case r: AwsRequestV1 => server.toRoute(getAllEndpoints)(r.toV2)
case r: AwsRequest => server.toRoute(getAllEndpoints)(r)
private lazy val server: AwsSyncServerInterpreter = AwsSyncServerInterpreter(options)
private lazy val route: AwsRequest => AwsResponse = server.toRoute(getAllEndpoints)
override def handleRequest(input: InputStream, output: OutputStream, context: Context): Unit = {
val allBytes = input.readAllBytes()
val decoded = decode[R](new String(allBytes, StandardCharsets.UTF_8))
val response = decoded match {
case Left(e) => AwsResponse.badRequest(s"Invalid AWS request: ${e.getMessage}")
case Right(awsRequest) =>
awsRequest match {
case r: AwsRequestV1 => route(r.toV2)
case r: AwsRequest => route(r)

Copilot uses AI. Check for mistakes.
case r: AwsRequestV1 => server.toRoute(getAllEndpoints)(r.toV2)
case r: AwsRequest => server.toRoute(getAllEndpoints)(r)
case r =>
throw new IllegalArgumentException(s"Request of type ${r.getClass.getCanonicalName} is not supported")
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

Throwing an IllegalArgumentException here will fail the whole Lambda invocation (often surfacing as a 502 from API Gateway) and can be hard to diagnose for callers. Prefer returning an AwsResponse.badRequest (or other 4xx) with a clear message, consistent with the JSON decode failure handling above.

Suggested change
throw new IllegalArgumentException(s"Request of type ${r.getClass.getCanonicalName} is not supported")
AwsResponse.badRequest(s"Request of type ${r.getClass.getCanonicalName} is not supported")

Copilot uses AI. Check for mistakes.
Comment on lines +2 to +5
"name": "tapir-sandbox",
"dockerComposeFile": "compose-all.yml",
"service": "agent",
"workspaceFolder": "/workspaces/tapir-sandbox",
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

The PR title/description focus on AWS Lambda interpreters, but this change switches the devcontainer to a new multi-container “sandcat” setup and adds a large number of new scripts/configs under .devcontainer/. If these dev-environment changes are intentional, they should be called out explicitly in the PR description; otherwise, consider splitting them into a separate PR to keep the Lambda interpreter change reviewable.

Copilot uses AI. Check for mistakes.
private-key <(echo "$client_private_key") \
fwmark 51820 \
peer "$server_public_key" \
endpoint mitmproxy:51820 \
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

The WireGuard peer endpoint is configured using the hostname mitmproxy:51820, but earlier the script resolves mitmproxy_ip specifically to avoid relying on DNS after routing changes. Consider setting the endpoint to the resolved IP (or otherwise ensuring DNS will still work here).

Suggested change
endpoint mitmproxy:51820 \
endpoint "$mitmproxy_ip":51820 \

Copilot uses AI. Check for mistakes.
image: ghcr.io/virtuslab/sandcat-mitmproxy-op:latest
command: mitmweb --mode wireguard --web-host 0.0.0.0 --set web_password=mitmproxy -s /scripts/mitmproxy_addon.py
ports:
- "8081" # mitmweb UI; host port assigned dynamically to avoid conflicts
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

In Docker Compose, the short port syntax "8081" maps host port 8081 to container port 8081 (equivalent to 8081:8081); it does not assign a random host port. Either update the comment or use a syntax that requests an ephemeral published port if avoiding conflicts is the goal.

Suggested change
- "8081" # mitmweb UI; host port assigned dynamically to avoid conflicts
- target: 8081
published: 0
protocol: tcp
mode: host # mitmweb UI; host port assigned dynamically to avoid conflicts

Copilot uses AI. Check for mistakes.
"anthropic.claude-code",
"github.vscode-pull-request-github",
"redhat.java",
"scalameta.metals",
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

devcontainer.json contains a trailing comma after the last entry in the extensions array. Unless this file is guaranteed to be parsed as JSONC everywhere, this makes it invalid JSON and can break tooling that expects strict JSON. Removing trailing commas (and keeping JSONC-specific features only when needed) will improve compatibility.

Suggested change
"scalameta.metals",
"scalameta.metals"

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +42
# Resolve the mitmproxy endpoint IP before setting up the tunnel, since DNS
# won't be available through the normal path after routing is configured.
mitmproxy_ip=$(getent hosts mitmproxy | awk '{print $1}')

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

mitmproxy_ip is resolved here with a comment explaining DNS may not be available later, but it is never used (the WireGuard peer endpoint is still set to mitmproxy:51820). Either use the resolved IP in the wg set command or remove the resolution/comment to avoid misleading future maintainers.

Copilot uses AI. Check for mistakes.
adamw and others added 5 commits March 27, 2026 16:48
The request decoding, V1→V2 normalization, and response writing logic was
duplicated across SyncLambdaHandler, LambdaHandler, and ZioLambdaHandler.
Extract it into a shared AwsLambdaCodec helper in lambda-core to prevent
behavioral drift between the handlers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move interpreter + toRoute call to a lazy val so the ServerInterpreter
is built once and reused across Lambda invocations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants