Skip to content

Commit 503aa76

Browse files
authored
make spin_sdk.http.IncomingHandler optional (spinframework#111)
When building a component intended for composition with other components, it may make sense to target the `spin-imports` world defined in the `src/spin_sdk/wit/spin.wit` file in this repo. However, that currently won't work because `spin_sdk.http` unconditionally imports `spin_sdk.wit.exports`, which won't exist when targeting `spin-imports`. The solution is to catch the `ImportError` if importing the `exports` submodule (or its `IncomingHandler` class) fails and just skip defining `spin_sdk.http.IncomingHandler` when that happens. Fixes spinframework#110 Signed-off-by: Joel Dice <joel.dice@fermyon.com>
1 parent b7e9caf commit 503aa76

File tree

1 file changed

+95
-87
lines changed

1 file changed

+95
-87
lines changed

src/spin_sdk/http/__init__.py

Lines changed: 95 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import traceback
55
from spin_sdk.http import poll_loop
66
from spin_sdk.http.poll_loop import PollLoop, Sink, Stream
7-
from spin_sdk.wit import exports
87
from spin_sdk.wit.types import Ok, Err
98
from spin_sdk.wit.imports.types import (
109
IncomingResponse, Method, Method_Get, Method_Head, Method_Post, Method_Put, Method_Delete, Method_Connect, Method_Options,
@@ -32,96 +31,105 @@ class Response:
3231
headers: MutableMapping[str, str]
3332
body: Optional[bytes]
3433

35-
class IncomingHandler(exports.IncomingHandler):
36-
"""Simplified handler for incoming HTTP requests using blocking, buffered I/O."""
34+
try:
35+
from spin_sdk.wit import exports
36+
from spin_sdk.wit.exports import IncomingHandler as Base
3737

38-
def handle_request(self, request: Request) -> Response:
39-
"""Handle an incoming HTTP request and return a response or raise an error"""
40-
raise NotImplementedError
38+
class IncomingHandler(Base):
39+
"""Simplified handler for incoming HTTP requests using blocking, buffered I/O."""
40+
41+
def handle_request(self, request: Request) -> Response:
42+
"""Handle an incoming HTTP request and return a response or raise an error"""
43+
raise NotImplementedError
44+
45+
def handle(self, request: IncomingRequest, response_out: ResponseOutparam):
46+
method = request.method()
47+
48+
if isinstance(method, Method_Get):
49+
method_str = "GET"
50+
elif isinstance(method, Method_Head):
51+
method_str = "HEAD"
52+
elif isinstance(method, Method_Post):
53+
method_str = "POST"
54+
elif isinstance(method, Method_Put):
55+
method_str = "PUT"
56+
elif isinstance(method, Method_Delete):
57+
method_str = "DELETE"
58+
elif isinstance(method, Method_Connect):
59+
method_str = "CONNECT"
60+
elif isinstance(method, Method_Options):
61+
method_str = "OPTIONS"
62+
elif isinstance(method, Method_Trace):
63+
method_str = "TRACE"
64+
elif isinstance(method, Method_Patch):
65+
method_str = "PATCH"
66+
elif isinstance(method, Method_Other):
67+
method_str = method.value
68+
else:
69+
raise AssertionError
70+
71+
request_body = request.consume()
72+
request_stream = request_body.stream()
73+
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__()
80+
IncomingBody.finish(request_body)
81+
break
82+
else:
83+
raise e
84+
85+
request_uri = request.path_with_query()
86+
if request_uri is None:
87+
uri = "/"
88+
else:
89+
uri = request_uri
4190

42-
def handle(self, request: IncomingRequest, response_out: ResponseOutparam):
43-
method = request.method()
44-
45-
if isinstance(method, Method_Get):
46-
method_str = "GET"
47-
elif isinstance(method, Method_Head):
48-
method_str = "HEAD"
49-
elif isinstance(method, Method_Post):
50-
method_str = "POST"
51-
elif isinstance(method, Method_Put):
52-
method_str = "PUT"
53-
elif isinstance(method, Method_Delete):
54-
method_str = "DELETE"
55-
elif isinstance(method, Method_Connect):
56-
method_str = "CONNECT"
57-
elif isinstance(method, Method_Options):
58-
method_str = "OPTIONS"
59-
elif isinstance(method, Method_Trace):
60-
method_str = "TRACE"
61-
elif isinstance(method, Method_Patch):
62-
method_str = "PATCH"
63-
elif isinstance(method, Method_Other):
64-
method_str = method.value
65-
else:
66-
raise AssertionError
67-
68-
request_body = request.consume()
69-
request_stream = request_body.stream()
70-
body = bytearray()
71-
while True:
7291
try:
73-
body += request_stream.blocking_read(16 * 1024)
74-
except Err as e:
75-
if isinstance(e.value, StreamError_Closed):
76-
request_stream.__exit__()
77-
IncomingBody.finish(request_body)
78-
break
79-
else:
80-
raise e
81-
82-
request_uri = request.path_with_query()
83-
if request_uri is None:
84-
uri = "/"
85-
else:
86-
uri = request_uri
87-
88-
try:
89-
simple_response = self.handle_request(Request(
90-
method_str,
91-
uri,
92-
dict(map(lambda pair: (pair[0], str(pair[1], "utf-8")), request.headers().entries())),
93-
bytes(body)
94-
))
95-
except:
96-
traceback.print_exc()
97-
98-
response = OutgoingResponse(Fields())
99-
response.set_status_code(500)
100-
ResponseOutparam.set(response_out, Ok(response))
101-
return
102-
103-
if simple_response.headers.get('content-length') is None:
104-
content_length = len(simple_response.body) if simple_response.body is not None else 0
105-
simple_response.headers['content-length'] = str(content_length)
106-
107-
response = OutgoingResponse(Fields.from_list(list(map(
108-
lambda pair: (pair[0], bytes(pair[1], "utf-8")),
109-
simple_response.headers.items()
110-
))))
111-
response_body = response.body()
112-
response.set_status_code(simple_response.status)
113-
ResponseOutparam.set(response_out, Ok(response))
114-
response_stream = response_body.write()
115-
if simple_response.body is not None:
116-
MAX_BLOCKING_WRITE_SIZE = 4096
117-
offset = 0
118-
while offset < len(simple_response.body):
119-
count = min(len(simple_response.body) - offset, MAX_BLOCKING_WRITE_SIZE)
120-
response_stream.blocking_write_and_flush(simple_response.body[offset:offset+count])
121-
offset += count
122-
response_stream.__exit__()
123-
OutgoingBody.finish(response_body, None)
92+
simple_response = self.handle_request(Request(
93+
method_str,
94+
uri,
95+
dict(map(lambda pair: (pair[0], str(pair[1], "utf-8")), request.headers().entries())),
96+
bytes(body)
97+
))
98+
except:
99+
traceback.print_exc()
124100

101+
response = OutgoingResponse(Fields())
102+
response.set_status_code(500)
103+
ResponseOutparam.set(response_out, Ok(response))
104+
return
105+
106+
if simple_response.headers.get('content-length') is None:
107+
content_length = len(simple_response.body) if simple_response.body is not None else 0
108+
simple_response.headers['content-length'] = str(content_length)
109+
110+
response = OutgoingResponse(Fields.from_list(list(map(
111+
lambda pair: (pair[0], bytes(pair[1], "utf-8")),
112+
simple_response.headers.items()
113+
))))
114+
response_body = response.body()
115+
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__()
126+
OutgoingBody.finish(response_body, None)
127+
128+
except ImportError:
129+
# `spin_sdk.wit.exports` won't exist if the use is targeting `spin-imports`,
130+
# so just skip this part
131+
pass
132+
125133
def send(request: Request) -> Response:
126134
"""Send an HTTP request and return a response or raise an error"""
127135
loop = PollLoop()

0 commit comments

Comments
 (0)