From cb3cf0d69b0449dec66ad4650a21b808a29e1ac3 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Wed, 17 Jun 2026 17:38:58 +0900 Subject: [PATCH 1/4] fix: fix ReadableStream being wrapped by BindingsWrapper incorrectly --- .../cli/tests/bindings-test/src/test_r2.py | 16 ++++++++++++ packages/runtime-sdk/src/workers/_workers.py | 26 ++++++++++++++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/packages/cli/tests/bindings-test/src/test_r2.py b/packages/cli/tests/bindings-test/src/test_r2.py index f4bbdc5..e460722 100644 --- a/packages/cli/tests/bindings-test/src/test_r2.py +++ b/packages/cli/tests/bindings-test/src/test_r2.py @@ -375,3 +375,19 @@ async def test_none_options_list(env): await bucket.put("_test/none_list", "val") result = await bucket.list(None) assert len(result.objects) >= 1 + + +@pytest.mark.asyncio +async def test_body_direct_to_response(env): + from workers import Response + + bucket = env.BUCKET + await _cleanup_r2(bucket) + key = "_test/body_direct" + value = "stream me directly" + await bucket.put(key, value) + obj = await bucket.get(key) + assert obj is not None + resp = Response(obj.body, headers={"x-test": "ok"}) + text = await resp.text() + assert text == value diff --git a/packages/runtime-sdk/src/workers/_workers.py b/packages/runtime-sdk/src/workers/_workers.py index 684c853..28bc591 100644 --- a/packages/runtime-sdk/src/workers/_workers.py +++ b/packages/runtime-sdk/src/workers/_workers.py @@ -393,6 +393,15 @@ def _get_js_body(body): } +def _is_response_accepted_type(obj) -> bool: + """ + Check if the given object is an accepted type for a Response body. + """ + if hasattr(obj, "constructor"): + return obj.constructor.name in RESPONSE_ACCEPTED_TYPES + return False + + class Response(FetchResponse): """ This class represents the response to an HTTP request, with a similar API to that of the web @@ -415,7 +424,7 @@ def __init__( """ # Verify passed in types. if hasattr(body, "constructor"): - if body.constructor.name not in RESPONSE_ACCEPTED_TYPES: + if not _is_response_accepted_type(body): raise TypeError( f"Unsupported type in Response: {body.constructor.name}" ) @@ -1110,20 +1119,29 @@ class _BindingWrapper: def __init__(self, binding): self._binding = binding + def _should_wrap_nested_attribute(self, jsobj) -> bool: + if not isinstance(jsobj, JsProxy): + return False + + return not _is_response_accepted_type(jsobj) + def _convert_result(self, result): converted = python_from_rpc(result) # After python_from_rpc, some objects may still be JsProxy objects. - # For now, we wrap all of them with the _BindingWrapper (or a subclass of it) + # For now, we wrap all of them except the ones that are already accepted as responses + # with the _BindingWrapper (or a subclass of it) # so that accessing attributes on them will be properly converted. # TODO: This is a bit of a hack. We should revisit when there are more # bindings to support with different return types. - if isinstance(converted, JsProxy): + if self._should_wrap_nested_attribute(converted): return self.__class__(converted) if isinstance(converted, list): return [ - self.__class__(item) if isinstance(item, JsProxy) else item + self.__class__(item) + if self._should_wrap_nested_attribute(item) + else item for item in converted ] return converted From bfd43a3be3a58128b957dc9ed664d1623edc936b Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Wed, 17 Jun 2026 17:57:08 +0900 Subject: [PATCH 2/4] chore: address comments --- packages/runtime-sdk/src/workers/_workers.py | 38 ++++++++++---------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/runtime-sdk/src/workers/_workers.py b/packages/runtime-sdk/src/workers/_workers.py index 28bc591..1e13eec 100644 --- a/packages/runtime-sdk/src/workers/_workers.py +++ b/packages/runtime-sdk/src/workers/_workers.py @@ -392,14 +392,18 @@ def _get_js_body(body): "Response", } +# JS built-in types that should NOT be wrapped in _BindingWrapper. +# These have their own Python-side semantics (e.g. passed directly to Response()) +# and wrapping them breaks property access like `.constructor.name`. +_JS_PASSTHROUGH_TYPES = RESPONSE_ACCEPTED_TYPES | { + "Headers", +} -def _is_response_accepted_type(obj) -> bool: - """ - Check if the given object is an accepted type for a Response body. - """ + +def _get_js_constructor_name(obj) -> str | None: if hasattr(obj, "constructor"): - return obj.constructor.name in RESPONSE_ACCEPTED_TYPES - return False + return obj.constructor.name + return None class Response(FetchResponse): @@ -423,11 +427,9 @@ def __init__( https://developer.mozilla.org/en-US/docs/Web/API/Response/Response. """ # Verify passed in types. - if hasattr(body, "constructor"): - if not _is_response_accepted_type(body): - raise TypeError( - f"Unsupported type in Response: {body.constructor.name}" - ) + js_type = _get_js_constructor_name(body) + if js_type not in RESPONSE_ACCEPTED_TYPES: + raise TypeError(f"Unsupported type in Response: {js_type}") elif not isinstance(body, str | FormData | bytes) and body is not None: raise TypeError(f"Unsupported type in Response: {type(body).__name__}") @@ -1123,18 +1125,18 @@ def _should_wrap_nested_attribute(self, jsobj) -> bool: if not isinstance(jsobj, JsProxy): return False - return not _is_response_accepted_type(jsobj) + # TODO: This allowlist approach is a workaround. The long-term fix is to + # add dedicated Python wrappers for these types in python_from_rpc so they + # never reach _BindingWrapper in the first place. + js_type = _get_js_constructor_name(jsobj) + return js_type not in _JS_PASSTHROUGH_TYPES def _convert_result(self, result): converted = python_from_rpc(result) # After python_from_rpc, some objects may still be JsProxy objects. - # For now, we wrap all of them except the ones that are already accepted as responses - # with the _BindingWrapper (or a subclass of it) - # so that accessing attributes on them will be properly converted. - - # TODO: This is a bit of a hack. We should revisit when there are more - # bindings to support with different return types. + # We need to wrap them with _BindingWrapper (or a subclass of it) again + # to ensure that accessing attributes on them will be properly converted. if self._should_wrap_nested_attribute(converted): return self.__class__(converted) if isinstance(converted, list): From d37bbcc5b490186caedb0579725cdabb6d9c4978 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Thu, 18 Jun 2026 14:17:57 +0900 Subject: [PATCH 3/4] chore: check None --- packages/runtime-sdk/src/workers/_workers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-sdk/src/workers/_workers.py b/packages/runtime-sdk/src/workers/_workers.py index 1e13eec..d3b0f75 100644 --- a/packages/runtime-sdk/src/workers/_workers.py +++ b/packages/runtime-sdk/src/workers/_workers.py @@ -428,7 +428,7 @@ def __init__( """ # Verify passed in types. js_type = _get_js_constructor_name(body) - if js_type not in RESPONSE_ACCEPTED_TYPES: + if js_type and js_type not in RESPONSE_ACCEPTED_TYPES: raise TypeError(f"Unsupported type in Response: {js_type}") elif not isinstance(body, str | FormData | bytes) and body is not None: raise TypeError(f"Unsupported type in Response: {type(body).__name__}") From 0f3cb3ae03b6ae4c3fffcd8c5383017b9b8fa3bf Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Thu, 18 Jun 2026 14:33:34 +0900 Subject: [PATCH 4/4] chore: fix condition --- packages/runtime-sdk/src/workers/_workers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/runtime-sdk/src/workers/_workers.py b/packages/runtime-sdk/src/workers/_workers.py index d3b0f75..f55b603 100644 --- a/packages/runtime-sdk/src/workers/_workers.py +++ b/packages/runtime-sdk/src/workers/_workers.py @@ -428,8 +428,9 @@ def __init__( """ # Verify passed in types. js_type = _get_js_constructor_name(body) - if js_type and js_type not in RESPONSE_ACCEPTED_TYPES: - raise TypeError(f"Unsupported type in Response: {js_type}") + if js_type: + if js_type not in RESPONSE_ACCEPTED_TYPES: + raise TypeError(f"Unsupported type in Response: {js_type}") elif not isinstance(body, str | FormData | bytes) and body is not None: raise TypeError(f"Unsupported type in Response: {type(body).__name__}") @@ -1129,7 +1130,7 @@ def _should_wrap_nested_attribute(self, jsobj) -> bool: # add dedicated Python wrappers for these types in python_from_rpc so they # never reach _BindingWrapper in the first place. js_type = _get_js_constructor_name(jsobj) - return js_type not in _JS_PASSTHROUGH_TYPES + return js_type and js_type not in _JS_PASSTHROUGH_TYPES def _convert_result(self, result): converted = python_from_rpc(result)