Skip to content

fix(lambda): return a downloadable GetFunction Code.Location#1718

Open
Jongsic wants to merge 1 commit into
floci-io:mainfrom
Jongsic:fix/lambda-getfunction-code-location
Open

fix(lambda): return a downloadable GetFunction Code.Location#1718
Jongsic wants to merge 1 commit into
floci-io:mainfrom
Jongsic:fix/lambda-getfunction-code-location

Conversation

@Jongsic

@Jongsic Jongsic commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

GetFunction (GET /2015-03-31/functions/{name}) returned a hardcoded, fabricated real-AWS URL for Code.Location:

code.put("Location", "https://awslambda-" + region + "-tasks.s3." + region
        + ".amazonaws.com/" + fn.getFunctionName());

That URL points at real AWS S3, is not presigned, and 404s. Any client that follows Code.Location to download the deployment package — CLIs, tooling, and browser-only UIs — cannot fetch the code. Real AWS and LocalStack both return a working, downloadable URL.

Root cause: Floci extracts the uploaded zip to a directory and discards the original bytes, so there was nothing to serve.

Fix: on every successful code deploy, persist the exact original zip bytes into Floci's own S3 under a per-region tasks bucket (awslambda-<region>-tasks), keyed by snapshots/<account>/<functionName>. GetFunction now builds a path-style URL on the local endpoint from the incoming request via UriInfo, so it targets the same Floci the client is already talking to (mirrors LocalStack). RepositoryType: "S3" stays truthful.

  • Byte-exact: the served package hashes to the already-stored CodeSha256 (sha256(originalZipBytes)). Re-zipping the extracted dir would break that.
  • Reuses existing infra: internal S3Service + S3Controller's path-style GET /{bucket}/{key:.+} + global CORS. No new endpoint, no new serving code.
  • Centralized in extractZipCode, so it covers ZipFile, S3Bucket/S3Key source, and the reactive S3-update listener with one change. Stored only after handler-file verification passes, so a rejected deploy leaves no orphan object.

Lifecycle: the stable key means redeploys overwrite in place (no stale accumulation), and DeleteFunction best-effort deletes the object.

Scope / tradeoffs:

  • Hot-reload functions (S3Bucket == "hot-reload") have no zip, so no object is stored — Code.Location 404s for them, matching the "no downloadable package" reality.
  • Functions deployed before this change 404 until their next deploy (both NoSuchBucket/NoSuchKey → clean 404, same "can't download" state as before, just against the local endpoint).
  • The awslambda-<region>-tasks bucket becomes visible in ListBuckets (same as LocalStack). Hiding service buckets is a separate follow-up, out of scope here.

Type of change

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

AWS Compatibility

  • Incorrect behavior: GetFunction returned a fabricated real-AWS S3 URL for Code.Location that 404s and is not presigned, so the deployment package could not be downloaded by any client following the URL.
  • Verified against: AWS CLI aws-cli/2.31.38 (create-function with --zip-file, then get-function), following the returned Code.Location with curl. Response is 200 application/zip and the downloaded bytes' SHA-256 matches the function's CodeSha256 (byte-exact). Behavior mirrors LocalStack's path-style Code.Location.

Checklist

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

GetFunction returned a fabricated real-AWS S3 URL for Code.Location that
404s, so clients (and browser UIs) cannot download the deployment package.
Persist the uploaded zip in Floci's own S3 (awslambda-<region>-tasks) on
every successful code deploy and return a path-style URL on the local
endpoint (built from the request via UriInfo), matching AWS/LocalStack.
Bytes are exact, so the download hashes to the stored CodeSha256.

Centralized in extractZipCode so it covers ZipFile, S3-source, and the
reactive S3-update listener; hot-reload is excluded by design. The stored
object is overwritten on redeploy and removed on DeleteFunction.
@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes GetFunction's Code.Location field, which previously returned a hard-coded, non-presigned real-AWS S3 URL that always 404s. The fix persists the original zip bytes into Floci's internal S3 under a stable per-region tasks bucket after every successful deploy, and builds a downloadable path-style URL from the incoming request's base URI.

  • LambdaController: replaces the fabricated URL with one built via UriInfo.getBaseUri() + UriBuilder, pointing to Floci's own S3 endpoint — correct and minimal.
  • LambdaService: adds storeDeploymentPackage (called only after handler-file verification, so failed deploys leave no orphan objects) and best-effort S3 cleanup in deleteFunction; the region parameter is threaded through all extractZipCode call sites including the reactive S3 listener.
  • LambdaIntegrationTest: covers the full lifecycle — deploy → GetFunction → download → SHA-256 byte-exact check → redeploy overwrites → delete removes the stored package.

Confidence Score: 4/5

The core fix is correct and well-tested; the only concern is a silent exception swallow in the best-effort S3 cleanup path that could make post-delete S3 failures invisible.

The URL construction, package storage, and lifecycle (store-on-success, overwrite-on-redeploy, best-effort delete) are all sound and covered by the new integration test. The one rough edge is in deleteFunction: the S3 cleanup catch block swallows failures without logging, which contradicts the project's own coding rule and would hide any unexpected errors during cleanup. Everything else — the region threading, the outer warn log in storeDeploymentPackage, and the handler-verification gate before storing — looks correct.

LambdaService.java around the deleteFunction S3 cleanup catch block.

Important Files Changed

Filename Overview
src/main/java/io/github/hectorvent/floci/services/lambda/LambdaController.java Replaces the hard-coded fabricated AWS S3 URL with a path-style URL built from the incoming request's base URI via UriInfo, routing the download to Floci's own S3 endpoint. Change is minimal and correct.
src/main/java/io/github/hectorvent/floci/services/lambda/LambdaService.java Adds storeDeploymentPackage (called after handler verification passes) and best-effort S3 cleanup on delete. One catch block in deleteFunction swallows failures silently without logging, violating the project coding style.
src/test/java/io/github/hectorvent/floci/services/lambda/LambdaIntegrationTest.java Adds a thorough @Order(40) integration test covering create → download → SHA-256 byte-exact check → redeploy overwrites → delete cleans up the stored package end-to-end.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant LambdaController
    participant LambdaService
    participant S3Service

    Note over Client,S3Service: CreateFunction / UpdateFunctionCode
    Client->>LambdaController: POST /2015-03-31/functions (ZipFile)
    LambdaController->>LambdaService: createFunction(region, ...)
    LambdaService->>LambdaService: extractZipCodeBytes() - extract + verify handler
    LambdaService->>S3Service: createBucket(awslambda-region-tasks)
    LambdaService->>S3Service: putObject(tasks-bucket, snapshots/acct/fn, zipBytes)
    S3Service-->>LambdaService: OK
    LambdaService-->>LambdaController: LambdaFunction
    LambdaController-->>Client: 201 Created

    Note over Client,S3Service: GetFunction
    Client->>LambdaController: GET /2015-03-31/functions/name
    LambdaController->>LambdaService: getFunction(region, name)
    LambdaService-->>LambdaController: LambdaFunction
    LambdaController->>LambdaController: UriBuilder.fromUri(baseUri).path(tasksBucket).path(codeObjectKey)
    LambdaController-->>Client: 200 Code.Location pointing to local S3

    Note over Client,S3Service: Download package
    Client->>S3Service: GET /awslambda-region-tasks/snapshots/acct/fn
    S3Service-->>Client: 200 application/zip (byte-exact zip)

    Note over Client,S3Service: DeleteFunction
    Client->>LambdaController: DELETE /2015-03-31/functions/name
    LambdaController->>LambdaService: deleteFunction(region, name)
    LambdaService->>LambdaService: functionStore.delete() (synchronized)
    LambdaService->>S3Service: deleteObject(tasks-bucket, key) best-effort
    LambdaService-->>LambdaController: void
    LambdaController-->>Client: 204
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant LambdaController
    participant LambdaService
    participant S3Service

    Note over Client,S3Service: CreateFunction / UpdateFunctionCode
    Client->>LambdaController: POST /2015-03-31/functions (ZipFile)
    LambdaController->>LambdaService: createFunction(region, ...)
    LambdaService->>LambdaService: extractZipCodeBytes() - extract + verify handler
    LambdaService->>S3Service: createBucket(awslambda-region-tasks)
    LambdaService->>S3Service: putObject(tasks-bucket, snapshots/acct/fn, zipBytes)
    S3Service-->>LambdaService: OK
    LambdaService-->>LambdaController: LambdaFunction
    LambdaController-->>Client: 201 Created

    Note over Client,S3Service: GetFunction
    Client->>LambdaController: GET /2015-03-31/functions/name
    LambdaController->>LambdaService: getFunction(region, name)
    LambdaService-->>LambdaController: LambdaFunction
    LambdaController->>LambdaController: UriBuilder.fromUri(baseUri).path(tasksBucket).path(codeObjectKey)
    LambdaController-->>Client: 200 Code.Location pointing to local S3

    Note over Client,S3Service: Download package
    Client->>S3Service: GET /awslambda-region-tasks/snapshots/acct/fn
    S3Service-->>Client: 200 application/zip (byte-exact zip)

    Note over Client,S3Service: DeleteFunction
    Client->>LambdaController: DELETE /2015-03-31/functions/name
    LambdaController->>LambdaService: deleteFunction(region, name)
    LambdaService->>LambdaService: functionStore.delete() (synchronized)
    LambdaService->>S3Service: deleteObject(tasks-bucket, key) best-effort
    LambdaService-->>LambdaController: void
    LambdaController-->>Client: 204
Loading

Reviews (1): Last reviewed commit: "fix(lambda): return a downloadable GetFu..." | Re-trigger Greptile

Comment on lines +558 to +564
if (s3Service != null) {
try {
s3Service.deleteObject(tasksBucketName(region), codeObjectKey(fn));
} catch (Exception ignored) {
// best-effort cleanup
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Silent catch in deleteFunction cleanup violates the project's coding style. AGENTS.md states: "Never leave a catch block empty. If an exception is intentionally tolerated, log it with enough context to diagnose it later." The storeDeploymentPackage equivalent correctly uses LOG.warnv; the delete cleanup should do the same so failures are diagnosable.

Suggested change
if (s3Service != null) {
try {
s3Service.deleteObject(tasksBucketName(region), codeObjectKey(fn));
} catch (Exception ignored) {
// best-effort cleanup
}
}
if (s3Service != null) {
try {
s3Service.deleteObject(tasksBucketName(region), codeObjectKey(fn));
} catch (Exception e) {
// best-effort cleanup
LOG.warnv("Could not delete deployment package for {0}: {1}",
functionName, e.getMessage());
}
}

Context Used: AGENTS.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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.

1 participant