Skip to content

Commit 8f3feb5

Browse files
authored
fix: parse bytes for callback operations (#59)
1 parent 4e88c54 commit 8f3feb5

File tree

8 files changed

+141
-130
lines changed

8 files changed

+141
-130
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
@@ -1411,13 +1411,13 @@ class SendDurableExecutionCallbackFailureRequest:
14111411
error: ErrorObject | None = None
14121412

14131413
@classmethod
1414-
def from_dict(cls, data: dict) -> SendDurableExecutionCallbackFailureRequest:
1415-
error = None
1416-
if error_data := data.get("Error"):
1417-
error = ErrorObject.from_dict(error_data)
1414+
def from_dict(
1415+
cls, data: dict, callback_id: str
1416+
) -> SendDurableExecutionCallbackFailureRequest:
1417+
error = ErrorObject.from_dict(data) if data else None
14181418

14191419
return cls(
1420-
callback_id=data["CallbackId"],
1420+
callback_id=callback_id,
14211421
error=error,
14221422
)
14231423

src/aws_durable_execution_sdk_python_testing/web/handlers.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import json
56
import logging
67
from abc import ABC, abstractmethod
78
from typing import TYPE_CHECKING, Any, cast
@@ -27,7 +28,6 @@
2728
SendDurableExecutionCallbackFailureResponse,
2829
SendDurableExecutionCallbackHeartbeatRequest,
2930
SendDurableExecutionCallbackHeartbeatResponse,
30-
SendDurableExecutionCallbackSuccessRequest,
3131
SendDurableExecutionCallbackSuccessResponse,
3232
StartDurableExecutionInput,
3333
StartDurableExecutionOutput,
@@ -37,7 +37,6 @@
3737
from aws_durable_execution_sdk_python_testing.web.models import (
3838
HTTPRequest,
3939
HTTPResponse,
40-
parse_json_body,
4140
)
4241
from aws_durable_execution_sdk_python_testing.web.routes import (
4342
CallbackFailureRoute,
@@ -92,9 +91,21 @@ def _parse_json_body(self, request: HTTPRequest) -> dict[str, Any]:
9291
dict: The parsed JSON data
9392
9493
Raises:
95-
ValueError: If the request body is empty or invalid JSON
94+
InvalidParameterValueException: If the request body is empty or invalid JSON
9695
"""
97-
return parse_json_body(request)
96+
if not request.body:
97+
msg = "Request body is required"
98+
raise InvalidParameterValueException(msg)
99+
100+
# Handle both dict and bytes body types
101+
if isinstance(request.body, dict):
102+
return request.body
103+
104+
try:
105+
return json.loads(request.body.decode("utf-8"))
106+
except (json.JSONDecodeError, UnicodeDecodeError) as e:
107+
msg = f"Invalid JSON in request body: {e}"
108+
raise InvalidParameterValueException(msg) from e
98109

99110
def _json_response(
100111
self,
@@ -631,20 +642,24 @@ def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse:
631642
HTTPResponse: The HTTP response to send to the client
632643
"""
633644
try:
634-
body_data: dict[str, Any] = self._parse_json_body(request)
635-
callback_request: SendDurableExecutionCallbackSuccessRequest = (
636-
SendDurableExecutionCallbackSuccessRequest.from_dict(body_data)
637-
)
638-
639645
callback_route = cast(CallbackSuccessRoute, parsed_route)
640646
callback_id: str = callback_route.callback_id
641647

648+
# For binary payload operations, body is raw bytes
649+
result_bytes = request.body if isinstance(request.body, bytes) else b""
650+
642651
callback_response: SendDurableExecutionCallbackSuccessResponse = ( # noqa: F841
643652
self.executor.send_callback_success(
644-
callback_id=callback_id, result=callback_request.result
653+
callback_id=callback_id, result=result_bytes
645654
)
646655
)
647656

657+
logger.debug(
658+
"Callback %s succeeded with result: %s",
659+
callback_id,
660+
result_bytes.decode("utf-8", errors="replace"),
661+
)
662+
648663
# Callback success response is empty
649664
return self._success_response({})
650665

@@ -672,20 +687,26 @@ def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse:
672687
HTTPResponse: The HTTP response to send to the client
673688
"""
674689
try:
690+
callback_route = cast(CallbackFailureRoute, parsed_route)
691+
callback_id: str = callback_route.callback_id
692+
675693
body_data: dict[str, Any] = self._parse_json_body(request)
676694
callback_request: SendDurableExecutionCallbackFailureRequest = (
677-
SendDurableExecutionCallbackFailureRequest.from_dict(body_data)
695+
SendDurableExecutionCallbackFailureRequest.from_dict(
696+
body_data, callback_id
697+
)
678698
)
679699

680-
callback_route = cast(CallbackFailureRoute, parsed_route)
681-
callback_id: str = callback_route.callback_id
682-
683700
callback_response: SendDurableExecutionCallbackFailureResponse = ( # noqa: F841
684701
self.executor.send_callback_failure(
685702
callback_id=callback_id, error=callback_request.error
686703
)
687704
)
688705

706+
logger.debug(
707+
"Callback %s failed with error: %s", callback_id, callback_request.error
708+
)
709+
689710
# Callback failure response is empty
690711
return self._success_response({})
691712

src/aws_durable_execution_sdk_python_testing/web/models.py

Lines changed: 27 additions & 21 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(
@@ -269,22 +294,3 @@ def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse:
269294
HTTPResponse: The HTTP response to send to the client
270295
"""
271296
... # pragma: no cover
272-
273-
274-
def parse_json_body(request: HTTPRequest) -> dict[str, Any]:
275-
"""Parse JSON body from HTTP request.
276-
277-
Args:
278-
request: The HTTP request containing the dict body
279-
280-
Returns:
281-
dict: The parsed JSON data (now just returns the body directly)
282-
283-
Raises:
284-
ValueError: If the request body is empty
285-
"""
286-
if not request.body:
287-
msg = "Request body is required"
288-
raise InvalidParameterValueException(msg)
289-
290-
return request.body

src/aws_durable_execution_sdk_python_testing/web/routes.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,12 @@ def from_route(cls, route: Route) -> ListDurableExecutionsByFunctionRoute:
401401

402402

403403
@dataclass(frozen=True)
404-
class CallbackSuccessRoute(Route):
404+
class BytesPayloadRoute(Route):
405+
"""Base class for routes that handle raw bytes payloads instead of JSON."""
406+
407+
408+
@dataclass(frozen=True)
409+
class CallbackSuccessRoute(BytesPayloadRoute):
405410
"""Route: POST /2025-12-01/durable-execution-callbacks/{callback_id}/succeed"""
406411

407412
callback_id: str
@@ -444,7 +449,7 @@ def from_route(cls, route: Route) -> CallbackSuccessRoute:
444449

445450

446451
@dataclass(frozen=True)
447-
class CallbackFailureRoute(Route):
452+
class CallbackFailureRoute(BytesPayloadRoute):
448453
"""Route: POST /2025-12-01/durable-execution-callbacks/{callback_id}/fail"""
449454

450455
callback_id: str

src/aws_durable_execution_sdk_python_testing/web/server.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
HTTPResponse,
4343
)
4444
from aws_durable_execution_sdk_python_testing.web.routes import (
45+
BytesPayloadRoute,
4546
CallbackFailureRoute,
4647
CallbackHeartbeatRoute,
4748
CallbackSuccessRoute,
@@ -120,15 +121,25 @@ def _handle_request(self, method: str) -> None:
120121
self.rfile.read(content_length) if content_length > 0 else b""
121122
)
122123

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-
)
124+
# For callback operations, use raw bytes directly
125+
if isinstance(parsed_route, BytesPayloadRoute):
126+
request = HTTPRequest.from_raw_bytes(
127+
body_bytes=body_bytes,
128+
method=method,
129+
path=parsed_route,
130+
headers=dict(self.headers),
131+
query_params=query_params,
132+
)
133+
else:
134+
# Create strongly-typed HTTP request object with pre-parsed body
135+
request = HTTPRequest.from_bytes(
136+
body_bytes=body_bytes,
137+
operation_name=None,
138+
method=method,
139+
path=parsed_route,
140+
headers=dict(self.headers),
141+
query_params=query_params,
142+
)
132143

133144
# Handle request with appropriate handler
134145
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():

0 commit comments

Comments
 (0)