Skip to content

Commit 82f75ef

Browse files
vdicedicej
andcommitted
feat(spin_sdk): update http module
- update http module per latest wasi/spin wit - remove poll_loop.py Signed-off-by: Vaughn Dice <vdice@akamai.com> Co-authored-by: Joel Dice <joel.dice@akamai.com>
1 parent 0b58aef commit 82f75ef

2 files changed

Lines changed: 61 additions & 458 deletions

File tree

src/spin_sdk/http/__init__.py

Lines changed: 61 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
"""Module with helpers for wasi http"""
22

3-
import asyncio
43
import traceback
5-
from spin_sdk.http import poll_loop
6-
from spin_sdk.http.poll_loop import PollLoop, Sink, Stream
7-
from componentize_py_types import Ok, Err
8-
from spin_sdk.wit.imports.wasi_http_types_0_2_0 import (
9-
IncomingResponse, Method, Method_Get, Method_Head, Method_Post, Method_Put, Method_Delete, Method_Connect,
10-
Method_Options, Method_Trace, Method_Patch, Method_Other, IncomingRequest, IncomingBody, ResponseOutparam,
11-
OutgoingResponse, Fields, Scheme, Scheme_Http, Scheme_Https, Scheme_Other, OutgoingRequest, OutgoingBody
4+
import componentize_py_async_support
5+
from componentize_py_types import Ok, Result
6+
from componentize_py_async_support.streams import ByteStreamWriter
7+
from componentize_py_async_support.futures import FutureReader
8+
from spin_sdk import wit
9+
from spin_sdk.wit.imports import wasi_http_client_0_3_0_rc_2026_03_15 as client
10+
from spin_sdk.wit.imports.wasi_http_types_0_3_0_rc_2026_03_15 import (
11+
Method, Method_Get, Method_Head, Method_Post, Method_Put, Method_Delete, Method_Connect,
12+
Method_Options, Method_Trace, Method_Patch, Method_Other,
13+
Fields, Scheme, Scheme_Http, Scheme_Https, Scheme_Other, ErrorCode, Request as WasiRequest, Response as WasiResponse
1214
)
13-
from spin_sdk.wit.imports.wasi_io_streams_0_2_0 import StreamError_Closed
1415
from dataclasses import dataclass
1516
from collections.abc import MutableMapping
1617
from typing import Optional
@@ -33,17 +34,17 @@ class Response:
3334

3435
try:
3536
from spin_sdk.wit import exports
36-
from spin_sdk.wit.exports import WasiHttpIncomingHandler020 as Base
37+
from spin_sdk.wit.exports import WasiHttpHandler030Rc20260315 as Base
3738

38-
class IncomingHandler(Base):
39+
class Handler(Base):
3940
"""Simplified handler for incoming HTTP requests using blocking, buffered I/O."""
4041

41-
def handle_request(self, request: Request) -> Response:
42+
async def handle_request(self, request: Request) -> Response:
4243
"""Handle an incoming HTTP request and return a response or raise an error"""
4344
raise NotImplementedError
4445

45-
def handle(self, request: IncomingRequest, response_out: ResponseOutparam):
46-
method = request.method()
46+
async def handle(self, request: WasiRequest) -> WasiResponse:
47+
method = request.get_method()
4748

4849
if isinstance(method, Method_Get):
4950
method_str = "GET"
@@ -68,76 +69,54 @@ def handle(self, request: IncomingRequest, response_out: ResponseOutparam):
6869
else:
6970
raise AssertionError
7071

71-
request_body = request.consume()
72-
request_stream = request_body.stream()
72+
headers = request.get_headers().copy_all()
73+
request_uri = request.get_path_with_query()
74+
rx, trailers = WasiRequest.consume_body(request, _unit_future())
7375
body = bytearray()
74-
while True:
75-
try:
76-
body += request_stream.blocking_read(16 * 1024)
77-
except Err as e:
78-
if isinstance(e.value, StreamError_Closed):
79-
request_stream.__exit__(None, None, None)
80-
IncomingBody.finish(request_body)
81-
break
82-
else:
83-
raise e
84-
85-
request_uri = request.path_with_query()
76+
with rx:
77+
while not rx.writer_dropped:
78+
body += await rx.read(16 * 1024)
79+
8680
if request_uri is None:
8781
uri = "/"
8882
else:
8983
uri = request_uri
9084

9185
try:
92-
simple_response = self.handle_request(Request(
86+
simple_response = await self.handle_request(Request(
9387
method_str,
9488
uri,
95-
dict(map(lambda pair: (pair[0], str(pair[1], "utf-8")), request.headers().entries())),
89+
dict(map(lambda pair: (pair[0], str(pair[1], "utf-8")), headers)),
9690
bytes(body)
9791
))
9892
except:
9993
traceback.print_exc()
10094

101-
response = OutgoingResponse(Fields())
95+
response = WasiResponse.new(Fields(), None, _trailers_future())[0]
10296
response.set_status_code(500)
103-
ResponseOutparam.set(response_out, Ok(response))
104-
return
97+
return response
10598

10699
if simple_response.headers.get('content-length') is None:
107100
content_length = len(simple_response.body) if simple_response.body is not None else 0
108101
simple_response.headers['content-length'] = str(content_length)
109102

110-
response = OutgoingResponse(Fields.from_list(list(map(
103+
tx, rx = wit.byte_stream()
104+
componentize_py_async_support.spawn(copy(simple_response.body, tx))
105+
response = WasiResponse.new(Fields.from_list(list(map(
111106
lambda pair: (pair[0], bytes(pair[1], "utf-8")),
112107
simple_response.headers.items()
113-
))))
114-
response_body = response.body()
108+
))), rx, _trailers_future())[0]
109+
115110
response.set_status_code(simple_response.status)
116-
ResponseOutparam.set(response_out, Ok(response))
117-
response_stream = response_body.write()
118-
if simple_response.body is not None:
119-
MAX_BLOCKING_WRITE_SIZE = 4096
120-
offset = 0
121-
while offset < len(simple_response.body):
122-
count = min(len(simple_response.body) - offset, MAX_BLOCKING_WRITE_SIZE)
123-
response_stream.blocking_write_and_flush(simple_response.body[offset:offset+count])
124-
offset += count
125-
response_stream.__exit__(None, None, None)
126-
OutgoingBody.finish(response_body, None)
111+
return response
127112

128113
except ImportError:
129114
# `spin_sdk.wit.exports` won't exist if the use is targeting `spin-imports`,
130115
# so just skip this part
131116
pass
132117

133-
def send(request: Request) -> Response:
118+
async def send(request: Request) -> Response:
134119
"""Send an HTTP request and return a response or raise an error"""
135-
loop = PollLoop()
136-
asyncio.set_event_loop(loop)
137-
return loop.run_until_complete(send_async(request))
138-
139-
140-
async def send_async(request: Request) -> Response:
141120
match request.method:
142121
case "GET":
143122
method: Method = Method_Get()
@@ -188,7 +167,9 @@ async def send_async(request: Request) -> Response:
188167
headers_dict.items()
189168
))
190169

191-
outgoing_request = OutgoingRequest(Fields.from_list(headers))
170+
tx, rx = wit.byte_stream()
171+
componentize_py_async_support.spawn(copy(request.body, tx))
172+
outgoing_request = WasiRequest.new(Fields.from_list(headers), rx, _trailers_future(), None)[0]
192173
outgoing_request.set_method(method)
193174
outgoing_request.set_scheme(scheme)
194175
if url_parsed.netloc == '':
@@ -206,33 +187,30 @@ async def send_async(request: Request) -> Response:
206187
path_and_query += '?' + url_parsed.query
207188
outgoing_request.set_path_with_query(path_and_query)
208189

209-
outgoing_body = request.body if request.body is not None else bytearray()
210-
sink = Sink(outgoing_request.body())
211-
incoming_response: IncomingResponse = (await asyncio.gather(
212-
poll_loop.send(outgoing_request),
213-
send_and_close(sink, outgoing_body)
214-
))[0]
190+
incoming_response = await client.send(outgoing_request)
215191

216-
response_body = Stream(incoming_response.consume())
192+
status = incoming_response.get_status_code()
193+
rx, trailers = WasiResponse.consume_body(incoming_response, _unit_future())
217194
body = bytearray()
218-
while True:
219-
chunk = await response_body.next()
220-
if chunk is None:
221-
headers = incoming_response.headers()
222-
simple_response = Response(
223-
incoming_response.status(),
224-
dict(map(
225-
lambda pair: (pair[0], str(pair[1], "utf-8")),
226-
headers.entries()
227-
)),
228-
bytes(body)
229-
)
230-
headers.__exit__(None, None, None)
231-
incoming_response.__exit__(None, None, None)
232-
return simple_response
233-
else:
234-
body += chunk
235-
236-
async def send_and_close(sink: Sink, data: bytes):
237-
await sink.send(data)
238-
sink.close()
195+
with rx:
196+
while not rx.writer_dropped:
197+
body += await rx.read(16 * 1024)
198+
199+
return Response(
200+
status,
201+
dict(map(
202+
lambda pair: (pair[0], str(pair[1], "utf-8")),
203+
headers.entries()
204+
)),
205+
bytes(body)
206+
)
207+
208+
async def copy(bytes:bytes, tx:ByteStreamWriter):
209+
with tx:
210+
await tx.write_all(bytes)
211+
212+
def _trailers_future() -> FutureReader[Result[Optional[Fields], ErrorCode]]:
213+
return wit.result_option_wasi_http_types_0_3_0_rc_2026_03_15_fields_wasi_http_types_0_3_0_rc_2026_03_15_error_code_future(lambda: Ok(None))[1]
214+
215+
def _unit_future() -> FutureReader[Result[None, ErrorCode]]:
216+
return wit.result_unit_wasi_http_types_0_3_0_rc_2026_03_15_error_code_future(lambda: Ok(None))[1]

0 commit comments

Comments
 (0)