Skip to content

Commit 6603748

Browse files
committed
fix: parse bytes for callback operations
1 parent 72879ac commit 6603748

File tree

7 files changed

+132
-62
lines changed

7 files changed

+132
-62
lines changed

src/aws_durable_execution_sdk_python_testing/model.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1386,13 +1386,13 @@ class SendDurableExecutionCallbackFailureRequest:
13861386
error: ErrorObject | None = None
13871387

13881388
@classmethod
1389-
def from_dict(cls, data: dict) -> SendDurableExecutionCallbackFailureRequest:
1390-
error = None
1391-
if error_data := data.get("Error"):
1392-
error = ErrorObject.from_dict(error_data)
1389+
def from_dict(
1390+
cls, data: dict, callback_id: str
1391+
) -> SendDurableExecutionCallbackFailureRequest:
1392+
error = ErrorObject.from_dict(data) if data else None
13931393

13941394
return cls(
1395-
callback_id=data["CallbackId"],
1395+
callback_id=callback_id,
13961396
error=error,
13971397
)
13981398

src/aws_durable_execution_sdk_python_testing/web/handlers.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
SendDurableExecutionCallbackFailureResponse,
2828
SendDurableExecutionCallbackHeartbeatRequest,
2929
SendDurableExecutionCallbackHeartbeatResponse,
30-
SendDurableExecutionCallbackSuccessRequest,
3130
SendDurableExecutionCallbackSuccessResponse,
3231
StartDurableExecutionInput,
3332
StartDurableExecutionOutput,
@@ -631,20 +630,24 @@ def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse:
631630
HTTPResponse: The HTTP response to send to the client
632631
"""
633632
try:
634-
body_data: dict[str, Any] = self._parse_json_body(request)
635-
callback_request: SendDurableExecutionCallbackSuccessRequest = (
636-
SendDurableExecutionCallbackSuccessRequest.from_dict(body_data)
637-
)
638-
639633
callback_route = cast(CallbackSuccessRoute, parsed_route)
640634
callback_id: str = callback_route.callback_id
641635

636+
# For binary payload operations, body is raw bytes
637+
result_bytes = request.body if isinstance(request.body, bytes) else b""
638+
642639
callback_response: SendDurableExecutionCallbackSuccessResponse = ( # noqa: F841
643640
self.executor.send_callback_success(
644-
callback_id=callback_id, result=callback_request.result
641+
callback_id=callback_id, result=result_bytes
645642
)
646643
)
647644

645+
logger.debug(
646+
"Callback %s succeeded with result: %s",
647+
callback_id,
648+
result_bytes.decode("utf-8", errors="replace"),
649+
)
650+
648651
# Callback success response is empty
649652
return self._success_response({})
650653

@@ -672,20 +675,26 @@ def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse:
672675
HTTPResponse: The HTTP response to send to the client
673676
"""
674677
try:
678+
callback_route = cast(CallbackFailureRoute, parsed_route)
679+
callback_id: str = callback_route.callback_id
680+
675681
body_data: dict[str, Any] = self._parse_json_body(request)
676682
callback_request: SendDurableExecutionCallbackFailureRequest = (
677-
SendDurableExecutionCallbackFailureRequest.from_dict(body_data)
683+
SendDurableExecutionCallbackFailureRequest.from_dict(
684+
body_data, callback_id
685+
)
678686
)
679687

680-
callback_route = cast(CallbackFailureRoute, parsed_route)
681-
callback_id: str = callback_route.callback_id
682-
683688
callback_response: SendDurableExecutionCallbackFailureResponse = ( # noqa: F841
684689
self.executor.send_callback_failure(
685690
callback_id=callback_id, error=callback_request.error
686691
)
687692
)
688693

694+
logger.debug(
695+
"Callback %s failed with error: %s", callback_id, callback_request.error
696+
)
697+
689698
# Callback failure response is empty
690699
return self._success_response({})
691700

src/aws_durable_execution_sdk_python_testing/web/models.py

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,38 @@
2525

2626
@dataclass(frozen=True)
2727
class HTTPRequest:
28-
"""HTTP request data model with dict body for handler logic."""
28+
"""HTTP request data model with dict or bytes body for handler logic."""
2929

3030
method: str
3131
path: Route
3232
headers: dict[str, str]
3333
query_params: dict[str, list[str]]
34-
body: dict[str, Any]
34+
body: dict[str, Any] | bytes
35+
36+
@classmethod
37+
def from_raw_bytes(
38+
cls,
39+
body_bytes: bytes,
40+
method: str = "POST",
41+
path: Route | None = None,
42+
headers: dict[str, str] | None = None,
43+
query_params: dict[str, list[str]] | None = None,
44+
) -> HTTPRequest:
45+
"""Create HTTPRequest with raw bytes body (no parsing)."""
46+
if headers is None:
47+
headers = {}
48+
if query_params is None:
49+
query_params = {}
50+
if path is None:
51+
path = Route.from_string("")
52+
53+
return cls(
54+
method=method,
55+
path=path,
56+
headers=headers,
57+
query_params=query_params,
58+
body=body_bytes,
59+
)
3560

3661
@classmethod
3762
def from_bytes(
@@ -275,16 +300,24 @@ def parse_json_body(request: HTTPRequest) -> dict[str, Any]:
275300
"""Parse JSON body from HTTP request.
276301
277302
Args:
278-
request: The HTTP request containing the dict body
303+
request: The HTTP request containing the JSON body
279304
280305
Returns:
281-
dict: The parsed JSON data (now just returns the body directly)
306+
dict: The parsed JSON data
282307
283308
Raises:
284-
ValueError: If the request body is empty
309+
ValueError: If the request body is empty or invalid JSON
285310
"""
286311
if not request.body:
287312
msg = "Request body is required"
288313
raise InvalidParameterValueException(msg)
289314

290-
return request.body
315+
# Handle both dict and bytes body types
316+
if isinstance(request.body, dict):
317+
return request.body
318+
319+
try:
320+
return json.loads(request.body.decode("utf-8"))
321+
except (json.JSONDecodeError, UnicodeDecodeError) as e:
322+
msg = f"Invalid JSON in request body: {e}"
323+
raise InvalidParameterValueException(msg) from e

src/aws_durable_execution_sdk_python_testing/web/server.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,27 @@ def _handle_request(self, method: str) -> None:
120120
self.rfile.read(content_length) if content_length > 0 else b""
121121
)
122122

123-
# Create strongly-typed HTTP request object with pre-parsed body
124-
request: HTTPRequest = HTTPRequest.from_bytes(
125-
body_bytes=body_bytes,
126-
operation_name=None, # Could be enhanced to map routes to AWS operation names
127-
method=method,
128-
path=parsed_route,
129-
headers=dict(self.headers),
130-
query_params=query_params,
131-
)
123+
# For callback operations, use raw bytes directly
124+
if "/durable-execution-callbacks/" in self.path and (
125+
self.path.endswith("/succeed") or self.path.endswith("/fail")
126+
):
127+
request = HTTPRequest.from_raw_bytes(
128+
body_bytes=body_bytes,
129+
method=method,
130+
path=parsed_route,
131+
headers=dict(self.headers),
132+
query_params=query_params,
133+
)
134+
else:
135+
# Create strongly-typed HTTP request object with pre-parsed body
136+
request = HTTPRequest.from_bytes(
137+
body_bytes=body_bytes,
138+
operation_name=None,
139+
method=method,
140+
path=parsed_route,
141+
headers=dict(self.headers),
142+
query_params=query_params,
143+
)
132144

133145
# Handle request with appropriate handler
134146
response: HTTPResponse = handler.handle(parsed_route, request)

tests/model_test.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -798,32 +798,38 @@ def test_send_durable_execution_callback_success_response_creation():
798798

799799
def test_send_durable_execution_callback_failure_request_serialization():
800800
"""Test SendDurableExecutionCallbackFailureRequest from_dict/to_dict round-trip."""
801-
data = {
802-
"CallbackId": "callback-123",
803-
"Error": {"ErrorMessage": "callback failed"},
804-
}
801+
data = {"ErrorMessage": "callback failed"}
805802

806-
request_obj = SendDurableExecutionCallbackFailureRequest.from_dict(data)
803+
request_obj = SendDurableExecutionCallbackFailureRequest.from_dict(
804+
data, "callback-123"
805+
)
807806
assert request_obj.callback_id == "callback-123"
808807
assert request_obj.error.message == "callback failed"
809808

810809
result_data = request_obj.to_dict()
811-
assert result_data == data
810+
expected_data = {
811+
"CallbackId": "callback-123",
812+
"Error": {"ErrorMessage": "callback failed"},
813+
}
814+
assert result_data == expected_data
812815

813816
# Test round-trip
814-
round_trip = SendDurableExecutionCallbackFailureRequest.from_dict(result_data)
817+
round_trip = SendDurableExecutionCallbackFailureRequest.from_dict(
818+
result_data.get("Error", {}), result_data["CallbackId"]
819+
)
815820
assert round_trip == request_obj
816821

817822

818823
def test_send_durable_execution_callback_failure_request_minimal():
819824
"""Test SendDurableExecutionCallbackFailureRequest with only required fields."""
820-
data = {"CallbackId": "callback-123"}
821825

822-
request_obj = SendDurableExecutionCallbackFailureRequest.from_dict(data)
826+
request_obj = SendDurableExecutionCallbackFailureRequest.from_dict(
827+
{}, "callback-123"
828+
)
823829
assert request_obj.error is None
824830

825831
result_data = request_obj.to_dict()
826-
assert result_data == data
832+
assert result_data == {"CallbackId": "callback-123"}
827833

828834

829835
def test_send_durable_execution_callback_failure_response_creation():

tests/web/handlers_test.py

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1957,13 +1957,13 @@ def test_send_durable_execution_callback_success_handler():
19571957
assert isinstance(route, CallbackSuccessRoute)
19581958
assert route.callback_id == "test-callback-id"
19591959

1960-
# Test with valid request body
1960+
# Test with valid request body (bytes for callback operations)
19611961
request = HTTPRequest(
19621962
method="POST",
19631963
path=route,
19641964
headers={"Content-Type": "application/json"},
19651965
query_params={},
1966-
body={"CallbackId": "test-callback-id", "Result": "success-result"},
1966+
body=b"success-result",
19671967
)
19681968

19691969
response = handler.handle(route, request)
@@ -1974,33 +1974,40 @@ def test_send_durable_execution_callback_success_handler():
19741974

19751975
# Verify executor was called with correct parameters
19761976
executor.send_callback_success.assert_called_once_with(
1977-
callback_id="test-callback-id", result="success-result"
1977+
callback_id="test-callback-id", result=b"success-result"
19781978
)
19791979

19801980

19811981
def test_send_durable_execution_callback_success_handler_empty_body():
19821982
"""Test SendDurableExecutionCallbackSuccessHandler with empty body."""
19831983
executor = Mock()
1984+
executor.send_callback_success.return_value = (
1985+
SendDurableExecutionCallbackSuccessResponse()
1986+
)
19841987
handler = SendDurableExecutionCallbackSuccessHandler(executor)
19851988

1989+
base_route = Route.from_string(
1990+
"/2025-12-01/durable-execution-callbacks/test-id/succeed"
1991+
)
1992+
callback_route = CallbackSuccessRoute.from_route(base_route)
1993+
19861994
request = HTTPRequest(
19871995
method="POST",
1988-
path=Route.from_string(
1989-
"/2025-12-01/durable-execution-callbacks/test-id/succeed"
1990-
),
1996+
path=callback_route,
19911997
headers={},
19921998
query_params={},
1993-
body={},
1999+
body=b"",
19942000
)
19952001

1996-
response = handler.handle(
1997-
Route.from_string("/2025-12-01/durable-execution-callbacks/test-id/succeed"),
1998-
request,
2002+
response = handler.handle(callback_route, request)
2003+
# Handler should accept empty body (Result is optional) and return 200
2004+
assert response.status_code == 200
2005+
assert response.body == {}
2006+
2007+
# Verify executor was called with empty result
2008+
executor.send_callback_success.assert_called_once_with(
2009+
callback_id="test-id", result=b""
19992010
)
2000-
# Handler returns 400 for empty body with AWS-compliant format
2001-
assert response.status_code == 400
2002-
assert response.body["Type"] == "InvalidParameterValueException"
2003-
assert "Request body is required" in response.body["message"]
20042011

20052012

20062013
def test_send_durable_execution_callback_failure_handler():
@@ -2032,7 +2039,7 @@ def test_send_durable_execution_callback_failure_handler():
20322039
path=route,
20332040
headers={"Content-Type": "application/json"},
20342041
query_params={},
2035-
body={"CallbackId": "test-callback-id", "Error": error_data},
2042+
body=error_data, # Pass error data directly as body
20362043
)
20372044
response = handler.handle(route, request)
20382045

@@ -2152,18 +2159,20 @@ def test_send_durable_execution_callback_failure_handler_empty_body():
21522159
executor = Mock()
21532160
handler = SendDurableExecutionCallbackFailureHandler(executor)
21542161

2162+
base_route = Route.from_string(
2163+
"/2025-12-01/durable-execution-callbacks/test-id/fail"
2164+
)
2165+
callback_route = CallbackFailureRoute.from_route(base_route)
2166+
21552167
request = HTTPRequest(
21562168
method="POST",
2157-
path=Route.from_string("/2025-12-01/durable-execution-callbacks/test-id/fail"),
2169+
path=callback_route,
21582170
headers={},
21592171
query_params={},
21602172
body={},
21612173
)
21622174

2163-
response = handler.handle(
2164-
Route.from_string("/2025-12-01/durable-execution-callbacks/test-id/fail"),
2165-
request,
2166-
)
2175+
response = handler.handle(callback_route, request)
21672176
# Handler returns 400 for empty body with AWS-compliant format
21682177
assert response.status_code == 400
21692178
assert response.body["Type"] == "InvalidParameterValueException"
@@ -2533,7 +2542,7 @@ def test_callback_handlers_use_dataclass_serialization():
25332542
}
25342543

25352544
failure_request = SendDurableExecutionCallbackFailureRequest.from_dict(
2536-
{"CallbackId": "test-id"}
2545+
{}, "test-id"
25372546
)
25382547
assert failure_request.callback_id == "test-id"
25392548
assert failure_request.error is None

tests/web/models_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,7 @@ def test_http_request_from_bytes_preserves_field_names() -> None:
418418
request = HTTPRequest.from_bytes(body_bytes=body_bytes)
419419

420420
# Field names should be preserved as-is
421+
assert isinstance(request.body, dict)
421422
assert "ExecutionName" in request.body
422423
assert "FunctionName" in request.body
423424
assert request.body["ExecutionName"] == "test-execution"

0 commit comments

Comments
 (0)