11"""Module with helpers for wasi http"""
22
3- import asyncio
43import 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
1415from dataclasses import dataclass
1516from collections .abc import MutableMapping
1617from typing import Optional
@@ -33,17 +34,17 @@ class Response:
3334
3435try :
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
128113except 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