Skip to content

fix(lambda): report resolved executed version - #2026

Merged
hectorvent merged 1 commit into
floci-io:mainfrom
aniketshukla1:fix/lambda-executed-version
Aug 1, 2026
Merged

hectorvent merged 1 commit into
floci-io:mainfrom
aniketshukla1:fix/lambda-executed-version

Conversation

@aniketshukla1

@aniketshukla1 aniketshukla1 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #1989.

  • Carry the resolved Lambda target version through the invocation result.
  • Emit that version in X-Amz-Executed-Version instead of always reporting $LATEST.
  • Add a version-qualified DryRun regression test for the public HTTP endpoint.

Type of change

  • Bug fix (fix:)
  • New feature (feat:)
  • Breaking change (feat! or fix!)
  • Docs / chore

AWS Compatibility

Version-qualified and alias-qualified invokes previously reported $LATEST even when another published version ran. The header now reports the resolved version, including aliases that route to a version.

Checklist

  • ./mvnw test passes locally
  • New or updated integration test added
  • Commit messages follow Conventional Commits

Focused verification: ./mvnw -q -Dtest=LambdaVersionIntegrationTest test and ./mvnw -q -Dtest=LambdaIntegrationTest test (JDK 25).

@github-actions

Copy link
Copy Markdown

🎉 Thanks for your first pull request to Floci!

Your CI checks need a maintainer to approve them before they run. That is GitHub's standard gate on first-time contributors, not a problem with your PR — so if the checks look like they are doing nothing, that is why. Once a maintainer approves, CI and the compatibility suite start automatically. Nothing is needed from you in the meantime.

While you wait, a couple of things that make review faster:

  • Link the issue this fixes with Closes #N in the description
  • Commits follow Conventional Commits (feat(s3): ..., fix(dynamodb): ...)
  • Behaviour changes come with a test — see CONTRIBUTING.md

Come join us in Slack — it is the fastest way to reach maintainers if you get stuck, or want feedback on an approach before investing more time in it.

@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown

Greptile Summary

Fix X-Amz-Executed-Version to report the resolved Lambda version instead of always $LATEST.

  • InvokeResult carries executedVersion; LambdaService.invoke sets it from the resolved LambdaFunction.getVersion().
  • LambdaController emits that value on the invoke response header.
  • Integration test covers DryRun invoke of function:1 expecting header 1.

Confidence Score: 5/5

Safe to merge; the resolved version is set on every LambdaController invoke path and defaults correctly for $LATEST and published versions.

Controller always goes through LambdaService.invoke, which sets executedVersion from LambdaFunction.version (default "$LATEST", numeric after publish/alias resolve). Direct executor callers do not emit this header. No null or wrong-header failure remains on the public invoke path.

Important Files Changed

Filename Overview
src/main/java/io/github/hectorvent/floci/services/lambda/LambdaService.java After executor invoke, stamps resolved fn version onto InvokeResult for the HTTP header path.
src/main/java/io/github/hectorvent/floci/services/lambda/LambdaController.java Replaces hardcoded $LATEST with result.getExecutedVersion() on X-Amz-Executed-Version.
src/main/java/io/github/hectorvent/floci/services/lambda/model/InvokeResult.java Adds executedVersion field with getter/setter; controller path always sets it via LambdaService.
src/test/java/io/github/hectorvent/floci/services/lambda/LambdaVersionIntegrationTest.java Adds ordered DryRun test that version-qualified invoke returns X-Amz-Executed-Version 1.

Sequence Diagram

sequenceDiagram
  participant Client
  participant LambdaController
  participant LambdaService
  participant Executor as LambdaExecutorService
  Client->>LambdaController: POST .../functions/name:1/invocations
  LambdaController->>LambdaService: invoke(region, name:1, ...)
  LambdaService->>LambdaService: "resolveInvokeTarget → fn.version=1"
  LambdaService->>Executor: invoke(fn, payload, type)
  Executor-->>LambdaService: InvokeResult
  LambdaService->>LambdaService: setExecutedVersion(fn.getVersion())
  LambdaService-->>LambdaController: result
  LambdaController-->>Client: X-Amz-Executed-Version: 1
Loading

Reviews (1): Last reviewed commit: "fix(lambda): report resolved executed ve..." | Re-trigger Greptile

@pgermosen pgermosen left a comment

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.

Looks good
(added a new comments both are minors)

Clean, correctly-layered fix. X-Amz-Executed-Version was hardcoded to "$LATEST" at LambdaController.java:253 regardless of what actually ran; the resolved target now flows through InvokeResult instead. Three lines of real logic, in the one layer that knows the answer.

One process note: the checklist leaves ./mvnw test passes locally unticked, with focused runs of LambdaVersionIntegrationTest and LambdaIntegrationTest instead. I ran the full suite against this head, so that box is covered — see above.

Comment on lines +80 to +86
void invokePublishedVersionReturnsExecutedVersion() {
given()
.header("X-Amz-Invocation-Type", "DryRun")
.when()
.post(BASE_PATH + "/functions/" + FUNCTION_NAME + ":1/invocations")
.then()
.statusCode(204)

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.

No $LATEST baseline — which is the case that could actually regress.

The header used to be hardcoded "$LATEST", so an unqualified invoke was accidentally correct before and needs to stay correct now. Nothing asserts it, and it's the cheapest possible regression guard — it also covers the qualifier == null branch of resolveInvokeTarget, which the new test doesn't touch:

    @Test
    @Order(4)
    void invokeUnqualifiedReturnsLatest() {
        given()
            .header("X-Amz-Invocation-Type", "DryRun")
        .when()
            .post(BASE_PATH + "/functions/" + FUNCTION_NAME + "/invocations")
        .then()
            .statusCode(204)
            .header("X-Amz-Executed-Version", equalTo("$LATEST"));
    }

Comment on lines +615 to +616
InvokeResult result = executorService.invoke(fn, payload, type);
result.setExecutedVersion(fn.getVersion());

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.

Verified this is safe, noting it so the reasoning is on record.

Setting the field here rather than in the executor is the right call — LambdaService is the layer that resolved the qualifier, and LambdaExecutorService never sees it. Two things I checked because they'd have been silent failures:

  • fn.getVersion() can't be null: LambdaFunction.version defaults to "$LATEST" (LambdaFunction.java:42) and is only reassigned on publish (:934). That matters more than it looks — JAX-RS header(name, null) removes the header rather than sending it empty, so a null here would have silently dropped X-Amz-Executed-Version from every response, a regression from the old always-$LATEST behavior.
  • executorService.invoke returns a non-null InvokeResult on every path (DryRun 204, Event 202, and each executeSync branch), so this can't NPE.

Minor style point, take it or leave it: this is a set-after-construct on an object that otherwise arrives fully built by its constructor. Threading it through as a constructor arg would keep InvokeResult immutable-ish, but that touches five call sites for little gain — I'd leave it.

@aniketshukla1

Copy link
Copy Markdown
Contributor Author

@pgermosen Thanks for the review, much appreciated.

@aniketshukla1

Copy link
Copy Markdown
Contributor Author

@hectorvent , is there anything else needs to be done in this ? Let me know.
Thanks

@hectorvent hectorvent self-assigned this Aug 1, 2026

@hectorvent hectorvent left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@hectorvent
hectorvent merged commit 2889fbd into floci-io:main Aug 1, 2026
15 checks passed
@hectorvent

Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 1.6.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

abanna pushed a commit to abanna/floci that referenced this pull request Aug 6, 2026
# [1.6.0](floci-io/floci@1.5.34...1.6.0) (2026-08-06)

### Bug Fixes

* **1790:** better populate describe alarm responses ([floci-io#1792](floci-io#1792)) ([fcb22be](floci-io@fcb22be))
* **apigateway:** fall through to less specific resources on method mi… ([floci-io#1630](floci-io#1630)) ([96c7aa6](floci-io@96c7aa6)), closes [floci-io#1547](floci-io#1547)
* **apigateway:** import authorizers and security requirements from OpenAPI ([floci-io#1798](floci-io#1798)) ([91e06eb](floci-io@91e06eb))
* **apigateway:** use last duplicate header value ([floci-io#1806](floci-io#1806)) ([6a214e8](floci-io@6a214e8))
* **athena:** report the result CSV object key as OutputLocation ([floci-io#1895](floci-io#1895)) ([418ec1b](floci-io@418ec1b)), closes [floci-io#1752](floci-io#1752)
* **cloudformation:** AWS::IAM::ManagedPolicy exposes PolicyArn for Fn::GetAtt ([floci-io#2056](floci-io#2056)) ([f719033](floci-io@f719033))
* **cloudformation:** keep uniqueness suffix when truncating generated resource names ([floci-io#1802](floci-io#1802)) ([48b2139](floci-io@48b2139)), closes [floci-io#1825](floci-io#1825)
* **cloudformation:** preserve subnet public IP setting ([floci-io#2031](floci-io#2031)) ([b62d1e7](floci-io@b62d1e7))
* **cloudformation:** provision AWS::ApiGatewayV2::Authorizer and wire Route.AuthorizerId ([floci-io#1760](floci-io#1760)) ([7f51c74](floci-io@7f51c74)), closes [floci-io#1773](floci-io#1773)
* **cloudformation:** resolve Fn::GetAtt [Vpc, DefaultSecurityGroup] ([floci-io#1977](floci-io#1977)) ([bdad066](floci-io@bdad066)), closes [floci-io#1976](floci-io#1976)
* **cloudformation:** treat delete of an already-removed DynamoDB table or Lambda function as idempotent ([floci-io#1803](floci-io#1803)) ([5cb26ff](floci-io@5cb26ff))
* **codebuild:** stabilize start and retry responses ([floci-io#2052](floci-io#2052)) ([fa96b79](floci-io@fa96b79))
* **cognito:** reject unconfirmed users in SRP auth ([floci-io#2027](floci-io#2027)) ([733c72d](floci-io@733c72d))
* **dockerfile:** ensure docker-java compatibility ([floci-io#2096](floci-io#2096)) ([7ade290](floci-io@7ade290))
* **dynamodb:** resolve nested document paths in ADD and DELETE update actions ([floci-io#1908](floci-io#1908)) ([c052259](floci-io@c052259))
* **ec2:** DescribeKeyPairs returns InvalidKeyPair.NotFound for missing names/ids ([floci-io#1932](floci-io#1932)) ([7d96106](floci-io@7d96106)), closes [floci-io#1911](floci-io#1911)
* **ec2:** return AWS-parity MissingParameter for CreateSubnet without VpcId ([floci-io#2094](floci-io#2094)) ([88408d8](floci-io@88408d8)), closes [floci-io#2089](floci-io#2089)
* **ecs:** resolve Secrets Manager JSON-key selectors in task definition secrets ([floci-io#2133](floci-io#2133)) ([e3c180b](floci-io@e3c180b)), closes [floci-io#1912](floci-io#1912)
* **lambda:** carry the owning account into published version snapshots ([floci-io#2041](floci-io#2041)) ([f4e8bd8](floci-io@f4e8bd8)), closes [floci-io#2040](floci-io#2040)
* **lambda:** report resolved executed version ([floci-io#2026](floci-io#2026)) ([2889fbd](floci-io@2889fbd))
* **lambda:** reset volatile DynamoDB Streams ESM checkpoints on restart ([floci-io#2077](floci-io#2077)) ([847c30a](floci-io@847c30a))
* **pipes:** register kafka-clients reflection metadata for native image ([floci-io#2068](floci-io#2068)) ([ab73379](floci-io@ab73379))
* **s3:** accept a percent-encoded bucket/key separator in copy sources ([floci-io#2060](floci-io#2060)) ([a8ed218](floci-io@a8ed218)), closes [floci-io#2038](floci-io#2038)
* **s3:** apply CreateBucketConfiguration tags on bucket creation ([floci-io#2115](floci-io#2115)) ([79031a2](floci-io@79031a2))
* **s3:** include EventBridgeConfiguration in GetBucketNotification response ([floci-io#2072](floci-io#2072)) ([b8edd37](floci-io@b8edd37))
* **s3:** recognize virtual-hosted-style requests over HTTP/2 ([floci-io#1954](floci-io#1954)) ([5f7d4d2](floci-io@5f7d4d2)), closes [floci-io#1866](floci-io#1866)
* **ssm:** report invalid batch parameter names ([floci-io#2075](floci-io#2075)) ([04873bf](floci-io@04873bf))
* **tls:** reuse the persisted self-signed certificate across restarts ([floci-io#1799](floci-io#1799)) ([a4356f2](floci-io@a4356f2))

### Features

* Add AWSCloudFormationReadOnlyAccess as a managed policy. ([floci-io#2057](floci-io#2057)) ([0142dc4](floci-io@0142dc4))
* **apigateway:** support floci:override-id for v1 and v2 ([floci-io#2045](floci-io#2045)) ([02385da](floci-io@02385da)), closes [floci-io#1593](floci-io#1593)
* **bedrock:** add proxy backend for real LLM responses via OpenAI-compatible API ([floci-io#1789](floci-io#1789)) ([875e0f2](floci-io@875e0f2))
* **cloudcontrol:** include EC2 resource tags ([floci-io#1933](floci-io#1933)) ([205a8f1](floci-io@205a8f1))
* **cloudformation:** provision AWS::EC2::LaunchTemplate ([floci-io#1973](floci-io#1973)) ([4e0a815](floci-io@4e0a815)), closes [floci-io#1971](floci-io#1971)
* **cloudformation:** provision AWS::EC2::VPCGatewayAttachment ([floci-io#1972](floci-io#1972)) ([1a99721](floci-io@1a99721)), closes [floci-io#1970](floci-io#1970)
* **cloudformation:** provision AWS::SecretsManager::SecretTargetAttachment ([floci-io#1804](floci-io#1804)) ([62a576c](floci-io@62a576c))
* **cloudformation:** support AWS::Events::EventBus and EventBusPolicy ([floci-io#1794](floci-io#1794)) ([76f602f](floci-io@76f602f))
* **cognito:** implement ResendConfirmationCode ([floci-io#2071](floci-io#2071)) ([bb7c45c](floci-io@bb7c45c)), closes [floci-io#2067](floci-io#2067)
* **ec2:** added attach and detach volume support ([floci-io#1787](floci-io#1787)) ([2c74329](floci-io@2c74329))
* **ec2:** return an empty set for DescribeVpnGateways ([floci-io#1975](floci-io#1975)) ([3baf4f4](floci-io@3baf4f4)), closes [floci-io#1974](floci-io#1974)
* **iam:** list entities attached to managed policies ([floci-io#1808](floci-io#1808)) ([78ba5b3](floci-io@78ba5b3))
* **iam:** seed Amazon Bedrock managed policies ([floci-io#2034](floci-io#2034)) ([5874348](floci-io@5874348)), closes [floci-io#1559](floci-io#1559) [floci-io#1216](floci-io#1216)
* **iam:** seed the managed policies CDK bootstrap and the scenario stacks attach ([floci-io#2064](floci-io#2064)) ([43aea09](floci-io@43aea09)), closes [floci-io#2059](floci-io#2059) [floci-io#2057](floci-io#2057) [floci-io#2057](floci-io#2057)
* **kms:** implement ListKeyPolicies ([floci-io#2046](floci-io#2046)) ([760115d](floci-io@760115d)), closes [floci-io#1528](floci-io#1528)
* **lambda:** implement the Lambda Extensions API ([floci-io#1773](floci-io#1773)) ([9f0dec9](floci-io@9f0dec9))
* **lambda:** support extra /etc/hosts entries on Lambda launch ([floci-io#2073](floci-io#2073)) ([b543aa4](floci-io@b543aa4))
* **mwaa:** add Amazon MWAA emulation backed by real Airflow (LocalExecutor) ([floci-io#2086](floci-io#2086)) ([7e4e3e8](floci-io@7e4e3e8))
* **release:** one-button release cut from main and ECR Public versioned publishing ([floci-io#2127](floci-io#2127)) ([61011c0](floci-io@61011c0))
* **rum:** add CloudWatch RUM app-monitor service ([floci-io#1797](floci-io#1797)) ([aadc93e](floci-io@aadc93e))
* **sqs:** retain MessageGroupId on standard queue messages ([floci-io#1891](floci-io#1891)) ([a01b707](floci-io@a01b707)), closes [floci-io#1496](floci-io#1496)

### Performance Improvements

* **docker:** stop duplicating the native binary into a second image layer ([floci-io#2069](floci-io#2069)) ([9e90e5c](floci-io@9e90e5c))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working lambda AWS Lambda released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] ExecutedVersion is always $LATEST, even for a version- or alias-qualified invoke

3 participants