Skip to content

fix(local-lambda): accept documented Lambda DurableExecutionArn shape#9040

Merged
roger-zhangg merged 1 commit into
aws:developfrom
yaythomas:fix/local-lambda-http-service-path-converter
May 21, 2026
Merged

fix(local-lambda): accept documented Lambda DurableExecutionArn shape#9040
roger-zhangg merged 1 commit into
aws:developfrom
yaythomas:fix/local-lambda-http-service-path-converter

Conversation

@yaythomas
Copy link
Copy Markdown
Contributor

@yaythomas yaythomas commented May 21, 2026

Issue #, if available

Fixes #9039

Description of changes

The Lambda public API documents DurableExecutionArn as:

arn:([a-zA-Z0-9-]+):lambda:([a-zA-Z0-9-]+):(\d{12})
    :function:([a-zA-Z0-9_-]+)
    :(\$LATEST(?:\.PUBLISHED)?|[0-9]+)
    /durable-execution/([a-zA-Z0-9_-]+)/([a-zA-Z0-9_-]+)

API_GetDurableExecution.

That shape contains characters that boto's REST-JSON serializer percent-encodes inside non-greedy URI labels: /%2F, :%3A, $%24. Werkzeug decodes them back to their literal forms before route matching, so the default <string> converter — which does not match / — cannot match the resulting multi-segment path. Every Get/History/Stop and every callback succeed/fail/heartbeat returns PathNotFoundLocally before reaching the handler when sam-cli is given the ARN shape the public API itself documents. This is independent of any specific upstream emulator behaviour; it is a direct consequence of how <string> rules interact with the documented ARN.

Fix: switch the six DurableExecutionArn / CallbackId URI labels in LocalLambdaHttpService from <string> to <path:...>. Werkzeug picks the right rule via trailing-literal specificity (…/<path:arn>/history beats …/<path:arn>). The existing unquote() calls in handlers stay — they are idempotent on already-decoded values. Backwards-compatible with the legacy UUID-only shape.

Description of how you validated changes

  • New TestDurableArnShapeCompatibility drives the real Flask app via test_client across three ARN shapes (the documented public-API shape, the transitional <uuid>/<invocation-id> currently emitted by the public.ecr.aws/durable-functions/aws-durable-execution-emulator image, and the legacy bare UUID) for all six durable-execution and callback routes, asserting routing reaches the correct handler with the decoded value.
  • Updated test_create_service_endpoints mock assertions to match the new <path:...> route strings.
  • make pr (init, schema, black-check, lint, test-all): green locally on Python 3.11.11 — 9253 unit tests pass, 25 skipped, 28 subtests pass, coverage 94.16% ≥ 94% gate.
  • Full integration suite tests/integration/local/start_lambda/test_start_lambda_durable.py: all 18 tests pass (vs 17 failures pre-fix), end-to-end through Docker + the durable-functions emulator image.

Checklist

  • Add input/output type hints to new functions/methods (n/a — only route-string change + unit tests)
  • Write design document if the changes are not straightforward (n/a)
  • Write/update tests
  • make pr passes
  • make update-reproducible-reqs if dependencies were changed (n/a)
  • Write documentation (n/a)

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@yaythomas yaythomas requested a review from a team as a code owner May 21, 2026 20:43
@github-actions github-actions Bot added area/local/start-api sam local start-api command area/local/invoke sam local invoke command area/local/start-invoke pr/external stage/needs-triage Automatically applied to new issues and PRs, indicating they haven't been looked at. labels May 21, 2026
Copy link
Copy Markdown
Collaborator

@aws-sam-cli-bot aws-sam-cli-bot left a comment

Choose a reason for hiding this comment

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

Code Review Results

Reviewed: 3c1153a..05039de
Files: 3
Comments: 1


Comments on lines outside the diff:

[tests/integration/durable_integ_base.py:155] [BUG] The new regex narrows the ARN capture to [a-f0-9-]+, which does not include /. With the new <uuid>/<invocation-id> ARN format that this PR is explicitly enabling, this regex truncates the captured value to just the leading UUID and drops the invocation-id segment.

For example, given output:

ARN:      9a1bc86c-40b9-4688-86b4-d7ecaca41579/2a8bc667-bc0b-4b5e-8c78-26db9c5b4e33
  • Old regex \S+ → captures the full <uuid>/<invocation-id> ARN.
  • New regex [a-f0-9-]+ → captures only 9a1bc86c-40b9-4688-86b4-d7ecaca41579.

execution_arn is then passed to downstream CLI commands such as sam local execution history <execution_arn> (see tests/integration/local/invoke/test_invoke_durable.py:56,88) and sam local execution stop <execution_arn> (see tests/integration/local/execution/test_execution.py:64). Even if those commands still accept UUID-only ARNs for backwards compatibility (as the PR description states), this change actively prevents the integration tests from exercising the new full-ARN form end-to-end through the Flask routes that this PR just fixed — which defeats the regression-coverage value the integration suite was meant to provide here. If the emulator/SDK ever drops UUID-only backwards compatibility, these tests will silently start using a malformed ARN.

The original \S+ already correctly captured both old and new formats. Suggest reverting this hunk, or if a tighter pattern is desired, allow the embedded /:

arn_match = re.search(r"ARN:\s+([a-f0-9/-]+)", stdout_str)

@yaythomas yaythomas marked this pull request as draft May 21, 2026 20:50
@yaythomas yaythomas force-pushed the fix/local-lambda-http-service-path-converter branch from 05039de to 1556d11 Compare May 21, 2026 20:58
@yaythomas yaythomas changed the title fix(local-lambda): allow '/' in durable execution and callback URI labels fix(local-lambda): accept documented Lambda DurableExecutionArn shape May 21, 2026
The Lambda public API documents DurableExecutionArn as

    arn:([a-zA-Z0-9-]+):lambda:([a-zA-Z0-9-]+):(\d{12})
        :function:([a-zA-Z0-9_-]+)
        :(\$LATEST(?:\.PUBLISHED)?|[0-9]+)
        /durable-execution/([a-zA-Z0-9_-]+)/([a-zA-Z0-9_-]+)

(https://docs.aws.amazon.com/lambda/latest/api/API_GetDurableExecution.html)

That shape contains characters that boto's REST-JSON serializer
percent-encodes inside non-greedy URI labels: '/' -> %2F, ':' -> %3A,
'$' -> %24. Werkzeug decodes those back to their literal forms before
route matching, so the default <string> converter (which does not match
'/') cannot match the resulting multi-segment path. Every Get / History
/ Stop and every callback succeed / fail / heartbeat 404'd with
PathNotFoundLocally before reaching the handler when sam-cli was given
the ARN shape the API itself documents.

Switch the six DurableExecutionArn / CallbackId URI labels in
LocalLambdaHttpService from <string> to <path:...>. Werkzeug picks the
right rule via trailing-literal specificity (.../<path:arn>/history
beats .../<path:arn>). The existing unquote() calls in handlers stay --
idempotent on already-decoded values. Backwards-compatible with the
legacy UUID-only shape.

Adds TestDurableArnShapeCompatibility, which drives the real Flask app
via test_client across three ARN shapes (the documented one, the
transitional <uuid>/<invocation-id> currently emitted by the
durable-functions emulator, and the legacy bare UUID) for all six
routes plus the three callback actions, asserting routing reaches the
correct handler with the decoded value.

Verified locally:
  - make pr (init, schema, black-check, lint, test-all): green on
    Python 3.11.11 (9253 unit tests pass, 25 skipped, 28 subtests pass,
    coverage 94.16%)
  - tests/integration/local/start_lambda/test_start_lambda_durable.py:
    all 18 tests pass (was 17 failures pre-fix), end-to-end through
    the public.ecr.aws/durable-functions/aws-durable-execution-emulator
    Docker image

Fixes aws#9039
Copy link
Copy Markdown
Collaborator

@aws-sam-cli-bot aws-sam-cli-bot left a comment

Choose a reason for hiding this comment

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

Code Review Results

Reviewed: 3c1153a..1556d11
Files: 3
Comments: 1


Comments on lines outside the diff:

[tests/integration/durable_integ_base.py:155] [BUG] The capture group ([a-f0-9-]+) excludes / (and :, $), so it truncates exactly the characters this PR is introducing into DurableExecutionArn.

Per the PR description, ARNs are now minted as <uuid>/<invocation-id> and will eventually take the documented public shape arn:aws:lambda:<region>:<acct>:function:<name>:$LATEST/durable-execution/<name>/<id>. Against the printed line:

ARN: 9a1bc86c-40b9-4688-86b4-d7ecaca41579/2a8bc667-bc0b-4b5e-8c78-26db9c5b4e33

the new regex captures only 9a1bc86c-40b9-4688-86b4-d7ecaca41579 and silently drops the /<invocation-id> segment. The truncated value is then passed as the ARN argument to sam local execution history / stop via get_execution_history_command_list at line 60, which is the exact path this PR is fixing.

The previous regex \S+ already captured the full ARN correctly across all three shapes (legacy UUID-only, transitional uuid/invocation-id, and the documented Lambda public shape). Either keep \S+ or, if a tighter pattern is desired, broaden to include the characters the new ARN format actually uses, e.g.:

arn_match = re.search(r"ARN:\s+(\S+)", stdout_str)

This was raised in the prior automated review and has not been addressed.

@yaythomas yaythomas force-pushed the fix/local-lambda-http-service-path-converter branch from 1556d11 to d45a481 Compare May 21, 2026 21:00
@yaythomas yaythomas marked this pull request as ready for review May 21, 2026 21:01
@yaythomas
Copy link
Copy Markdown
Contributor Author

yaythomas commented May 21, 2026

the two review bots comments were because initially I based this branch on develop from before PR #9038 squash-merged, so the diff against current origin/develop showed
tests/integration/durable_integ_base.py reverting \S+ back to [a-f0-9-]+.

rebasing onto current origin/develop (commit d45a481) drops that spurious revert and leaves only the intended local_lambda_http_service.py route changes plus their unit tests.

tldr; bot reviews are stale.

@yaythomas
Copy link
Copy Markdown
Contributor Author

I ran this against previously failing integration tests locally:

$ SAM_CLI_DEV=1 SAM_CLI_TELEMETRY=0 pytest -vv tests/integration/local/start_lambda/test_start_lambda_durable.py

tests/integration/local/start_lambda/test_start_lambda_durable.py::TestStartLambdaDurable::test_local_callback_cli_fail_0_all_parameters PASSED                              [  5%]
tests/integration/local/start_lambda/test_start_lambda_durable.py::TestStartLambdaDurable::test_local_callback_cli_fail_1_minimal_parameters PASSED                          [ 11%]
tests/integration/local/start_lambda/test_start_lambda_durable.py::TestStartLambdaDurable::test_local_callback_cli_fail_2_error_message_only PASSED                          [ 16%]
tests/integration/local/start_lambda/test_start_lambda_durable.py::TestStartLambdaDurable::test_local_callback_cli_heartbeat PASSED                                          [ 22%]
tests/integration/local/start_lambda/test_start_lambda_durable.py::TestStartLambdaDurable::test_local_start_lambda_invoke_durable_function_HelloWorld PASSED                 [ 27%]
tests/integration/local/start_lambda/test_start_lambda_durable.py::TestStartLambdaDurable::test_local_start_lambda_invoke_durable_function_MapOperations PASSED              [ 33%]
tests/integration/local/start_lambda/test_start_lambda_durable.py::TestStartLambdaDurable::test_local_start_lambda_invoke_durable_function_NamedStep PASSED                  [ 38%]
tests/integration/local/start_lambda/test_start_lambda_durable.py::TestStartLambdaDurable::test_local_start_lambda_invoke_durable_function_NamedWait PASSED                  [ 44%]
tests/integration/local/start_lambda/test_start_lambda_durable.py::TestStartLambdaDurable::test_local_start_lambda_invoke_durable_function_Parallel PASSED                   [ 50%]
tests/integration/local/start_lambda/test_start_lambda_durable.py::TestStartLambdaDurable::test_local_start_lambda_invoke_timeout PASSED                                     [ 55%]
tests/integration/local/start_lambda/test_start_lambda_durable.py::TestStartLambdaDurable::test_local_start_lambda_invoke_wait_for_callback_failure_http_0_all_parameters PASSED  [ 61%]
tests/integration/local/start_lambda/test_start_lambda_durable.py::TestStartLambdaDurable::test_local_start_lambda_invoke_wait_for_callback_failure_http_1_minimal_parameters PASSED [ 66%]
tests/integration/local/start_lambda/test_start_lambda_durable.py::TestStartLambdaDurable::test_local_start_lambda_invoke_wait_for_callback_failure_http_2_error_message_only PASSED [ 72%]
tests/integration/local/start_lambda/test_start_lambda_durable.py::TestStartLambdaDurable::test_local_start_lambda_invoke_wait_for_callback_success_cli_0_with_result PASSED   [ 77%]
tests/integration/local/start_lambda/test_start_lambda_durable.py::TestStartLambdaDurable::test_local_start_lambda_invoke_wait_for_callback_success_cli_1_without_result PASSED [ 83%]
tests/integration/local/start_lambda/test_start_lambda_durable.py::TestStartLambdaDurable::test_local_start_lambda_invoke_wait_for_callback_success_http_0_with_result PASSED [ 88%]
tests/integration/local/start_lambda/test_start_lambda_durable.py::TestStartLambdaDurable::test_local_start_lambda_invoke_wait_for_callback_success_http_1_without_result PASSED [ 94%]
tests/integration/local/start_lambda/test_start_lambda_durable.py::TestStartLambdaDurable::test_local_start_lambda_invoke_wait_for_callback_timeout PASSED                   [100%]

============= 18 passed, 114 subtests passed in 103.34s (0:01:43) ==============

@roger-zhangg roger-zhangg added this pull request to the merge queue May 21, 2026
Merged via the queue into aws:develop with commit d97adde May 21, 2026
53 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/local/invoke sam local invoke command area/local/start-api sam local start-api command area/local/start-invoke pr/external stage/needs-triage Automatically applied to new issues and PRs, indicating they haven't been looked at.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Local Lambda HTTP service rejects DurableExecutionArn / CallbackId containing "/"

4 participants