Summary
dex-local-runner (WebRunner) returns 404 for every Get/State/History/Checkpoint/Stop call against an execution whose ARN contains a literal /, and silently returns an empty list for ListDurableExecutionsByFunction when the function name contains : or $. The cause is that the WebServer's route layer does not URL-decode captured path segments before using them as store-lookup keys, while boto's rest-json serializer percent-encodes those characters in non-greedy URI labels.
Affected versions
aws-durable-execution-sdk-python-testing == 1.2.0 (PyPI). The bug was introduced in #216 (which started minting ARNs of the form <uuid>/<invocation-id>) and shipped in v1.2.0.
Affected runners
WebRunner / dex-local-runner — broken for every nontrivial durable function, since the first context.step / context.wait / context.callback triggers a checkpoint call that 404s.
DurableFunctionTestRunner (in-process) — not affected; uses InMemoryServiceClient which ignores durable_execution_arn.
DurableFunctionCloudTestRunner (real AWS) — not affected; ARNs come from the real Lambda backend and never round-trip through the local route layer.
Affected operations
All five durable-execution operations whose URI label is the ARN, plus the by-function list:
| Operation |
URI template |
GetDurableExecution |
GET /2025-12-01/durable-executions/{DurableExecutionArn} |
GetDurableExecutionState |
GET /2025-12-01/durable-executions/{DurableExecutionArn}/state |
GetDurableExecutionHistory |
GET /2025-12-01/durable-executions/{DurableExecutionArn}/history |
CheckpointDurableExecution |
POST /2025-12-01/durable-executions/{DurableExecutionArn}/checkpoint |
StopDurableExecution |
POST /2025-12-01/durable-executions/{DurableExecutionArn}/stop |
ListDurableExecutionsByFunction |
GET /2025-12-01/functions/{FunctionName}/durable-executions |
For non-greedy labels (no +), boto encodes:
Reproduction
import boto3
from aws_durable_execution_sdk_python_testing import WebRunner, WebRunnerConfig
from aws_durable_execution_sdk_python_testing.web.server import WebServiceConfig
with WebRunner(WebRunnerConfig(web_service=WebServiceConfig(port=0))) as runner:
# ...start an execution via runner so a slash-containing ARN is minted...
arn = "<uuid>/<invocation-id>" # the format Execution.new() produces
client = boto3.client("lambda", endpoint_url="http://127.0.0.1:<port>", ...)
client.get_durable_execution(DurableExecutionArn=arn)
# botocore.exceptions.ClientError:
# An error occurred (404) when calling the GetDurableExecution operation:
# Execution <uuid>%2F<invocation-id> not found
The %2F literal in the error message is the giveaway — the WebServer is looking up the still-encoded form rather than the canonical ARN.
Why nothing caught it
- All routing tests use slash-free literals like
"test-arn" or "my-function", so Route.from_string never produced a segment containing %XX.
- All
boto3.client usages in the test suite are mocked (patch("boto3.client")), so the real botocore serializer that does the encoding is never exercised.
- The integration test in
tests/web/e2e/server_int_test.py constructs HTTPRequest objects in-process and bypasses the HTTP/socket layer.
- The headline e2e test (
tests/e2e/basic_success_path_test.py) uses DurableFunctionTestRunner, which doesn't go through the affected route layer.
- CI runs only
hatch test / hatch fmt --check / hatch run types:check; no test exercised the boto→WebServer seam.
#117 fixed the same bug shape for callback IDs but the systemic fix (decode at the parser layer) was not applied at the time.
Expected fix
URL-decode captured path segments at the parser layer (Route.from_string) so every existing and future captured field inherits the behavior, and add a real-boto-through-WebServer regression test so the seam stays covered. PR forthcoming.
Summary
dex-local-runner(WebRunner) returns404for every Get/State/History/Checkpoint/Stop call against an execution whose ARN contains a literal/, and silently returns an empty list forListDurableExecutionsByFunctionwhen the function name contains:or$. The cause is that the WebServer's route layer does not URL-decode captured path segments before using them as store-lookup keys, while boto's rest-json serializer percent-encodes those characters in non-greedy URI labels.Affected versions
aws-durable-execution-sdk-python-testing == 1.2.0(PyPI). The bug was introduced in #216 (which started minting ARNs of the form<uuid>/<invocation-id>) and shipped in v1.2.0.Affected runners
WebRunner/dex-local-runner— broken for every nontrivial durable function, since the firstcontext.step/context.wait/context.callbacktriggers a checkpoint call that 404s.DurableFunctionTestRunner(in-process) — not affected; usesInMemoryServiceClientwhich ignoresdurable_execution_arn.DurableFunctionCloudTestRunner(real AWS) — not affected; ARNs come from the real Lambda backend and never round-trip through the local route layer.Affected operations
All five durable-execution operations whose URI label is the ARN, plus the by-function list:
GetDurableExecutionGET /2025-12-01/durable-executions/{DurableExecutionArn}GetDurableExecutionStateGET /2025-12-01/durable-executions/{DurableExecutionArn}/stateGetDurableExecutionHistoryGET /2025-12-01/durable-executions/{DurableExecutionArn}/historyCheckpointDurableExecutionPOST /2025-12-01/durable-executions/{DurableExecutionArn}/checkpointStopDurableExecutionPOST /2025-12-01/durable-executions/{DurableExecutionArn}/stopListDurableExecutionsByFunctionGET /2025-12-01/functions/{FunctionName}/durable-executionsFor non-greedy labels (no
+), boto encodes:/→%2F:→%3A$→%24Reproduction
The
%2Fliteral in the error message is the giveaway — the WebServer is looking up the still-encoded form rather than the canonical ARN.Why nothing caught it
"test-arn"or"my-function", soRoute.from_stringnever produced a segment containing%XX.boto3.clientusages in the test suite are mocked (patch("boto3.client")), so the real botocore serializer that does the encoding is never exercised.tests/web/e2e/server_int_test.pyconstructsHTTPRequestobjects in-process and bypasses the HTTP/socket layer.tests/e2e/basic_success_path_test.py) usesDurableFunctionTestRunner, which doesn't go through the affected route layer.hatch test/hatch fmt --check/hatch run types:check; no test exercised the boto→WebServer seam.#117 fixed the same bug shape for callback IDs but the systemic fix (decode at the parser layer) was not applied at the time.
Expected fix
URL-decode captured path segments at the parser layer (
Route.from_string) so every existing and future captured field inherits the behavior, and add a real-boto-through-WebServer regression test so the seam stays covered. PR forthcoming.