Skip to content

Commit 8e35f64

Browse files
radu-mateiclaude
andcommitted
fix(stateful): read the WASIp3 request body instead of dropping it
The stateful HTTP handler hardcoded body = b"", so request bodies never reached handle_request. In WASIp3 the body is a stream<u8> obtained via the static request.consume-body; read it (consume_body moves the request, so method/uri/headers are read first) and drain the stream to bytes until the writer end closes, mirroring componentize-py's http-p3 example. The unit res-future constructor is discovered on the world module (the error_code future without "fields"). Requires componentize-py with WASI 0.3 support (bytecodealliance/componentize-py#225). py_compile + inspection verified; not runtime-tested here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 46734a1 commit 8e35f64

1 file changed

Lines changed: 51 additions & 12 deletions

File tree

src/spin_sdk/stateful.py

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,14 @@ def _get_http_types(request):
132132
def _get_world_helpers():
133133
"""Discover componentize-py's stream/future helpers on the world module.
134134
135-
componentize-py generates ``byte_stream()`` (for ``stream<u8>``) and a
136-
trailers-future constructor (for ``future<result<option<fields>,
137-
error-code>>``) on the world module. The trailers-future function has
138-
an auto-generated name, so we find it by inspecting the module.
135+
componentize-py generates ``byte_stream()`` (for ``stream<u8>``) and, for
136+
each distinct ``future`` type the world uses, a constructor with an
137+
auto-generated name. We need two of those futures, told apart by name:
138+
139+
- the **trailers** future ``future<result<option<fields>, error-code>>``,
140+
used when building a response, and
141+
- the **unit** future ``future<result<_, error-code>>``, which is the
142+
``res`` argument to ``request.consume-body`` (and ``response.consume-body``).
139143
"""
140144
global _world_helpers
141145
if _world_helpers is not None:
@@ -145,27 +149,50 @@ def _get_world_helpers():
145149

146150
byte_stream_fn = getattr(world, "byte_stream", None)
147151

152+
# wasi:http/types has exactly two `error-code` futures: the trailers future
153+
# `result<option<fields>, error-code>` (named with "fields") and the unit
154+
# future `result<_, error-code>` (everything else). Tell them apart by the
155+
# presence of "fields" rather than relying on the exact "unit" spelling.
148156
trailers_future_fn = None
157+
unit_future_fn = None
149158
for name in dir(world):
150-
if name.endswith("_future") and "fields" in name and "error_code" in name:
151-
trailers_future_fn = getattr(world, name)
152-
break
153-
154-
_world_helpers = (byte_stream_fn, trailers_future_fn)
159+
if not (name.endswith("_future") and "error_code" in name):
160+
continue
161+
if "fields" in name:
162+
trailers_future_fn = trailers_future_fn or getattr(world, name)
163+
else:
164+
unit_future_fn = unit_future_fn or getattr(world, name)
165+
166+
_world_helpers = (byte_stream_fn, trailers_future_fn, unit_future_fn)
155167
return _world_helpers
156168

157169

158170
def _make_trailers_future():
159171
"""Create a resolved trailers future indicating no trailers and no error."""
160172
from componentize_py_types import Ok
161-
_, trailers_future_fn = _get_world_helpers()
173+
_, trailers_future_fn, _ = _get_world_helpers()
162174
if trailers_future_fn is None:
163175
raise RuntimeError(
164176
"Could not find trailers-future constructor on the world module"
165177
)
166178
return trailers_future_fn(lambda: Ok(None))[1]
167179

168180

181+
def _make_unit_future():
182+
"""Create a resolved ``future<result<_, error-code>>`` (unit Ok, no error).
183+
184+
This is the ``res`` argument required by ``request.consume-body`` — it lets
185+
the caller signal an error in handling the request; we always resolve it Ok.
186+
"""
187+
from componentize_py_types import Ok
188+
_, _, unit_future_fn = _get_world_helpers()
189+
if unit_future_fn is None:
190+
raise RuntimeError(
191+
"Could not find unit result-future constructor on the world module"
192+
)
193+
return unit_future_fn(lambda: Ok(None))[1]
194+
195+
169196
def _make_body_stream(body_bytes):
170197
"""Wrap ``bytes`` in a WASIp3 ``stream<u8>`` readable end.
171198
@@ -175,7 +202,7 @@ def _make_body_stream(body_bytes):
175202
if not body_bytes:
176203
return None
177204

178-
byte_stream_fn, _ = _get_world_helpers()
205+
byte_stream_fn, _, _ = _get_world_helpers()
179206
if byte_stream_fn is None:
180207
raise RuntimeError(
181208
"Could not find byte_stream constructor on the world module"
@@ -219,7 +246,19 @@ async def handle(self, request):
219246
for name, value in headers_resource.copy_all()
220247
)
221248

222-
body = b""
249+
# Consume the incoming request body. In WASIp3 the body is a
250+
# `stream<u8>`: `consume-body` is a static func that *moves* the
251+
# request (so method/uri/headers must already be read, as above) and
252+
# returns the readable stream end plus a trailers future. Drain it to
253+
# bytes, reading until the writer end is closed.
254+
body_rx = http_types.Request.consume_body(request, _make_unit_future())[0]
255+
body_chunks = []
256+
with body_rx:
257+
while not body_rx.writer_dropped:
258+
chunk = await body_rx.read(64 * 1024)
259+
if chunk:
260+
body_chunks.append(chunk)
261+
body = b"".join(body_chunks)
223262

224263
try:
225264
result = _component_instance.handle_request(

0 commit comments

Comments
 (0)