Module spin_sdk.http

Module with helpers for wasi http

Sub-modules

spin_sdk.http.poll_loop

Defines a custom asyncio event loop backed by wasi:io/poll#poll

Functions

def send(request: Request) ‑> Response
Expand source code
def send(request: Request) -> Response:
    """Send an HTTP request and return a response or raise an error"""
    loop = PollLoop()
    asyncio.set_event_loop(loop)
    return loop.run_until_complete(send_async(request))

Send an HTTP request and return a response or raise an error

async def send_and_close(sink: Sink,
data: bytes)
Expand source code
async def send_and_close(sink: Sink, data: bytes):
    await sink.send(data)
    sink.close()
async def send_async(request: Request) ‑> Response
Expand source code
async def send_async(request: Request) -> Response:
    match request.method:
        case "GET":
            method: Method = Method_Get()
        case "HEAD":
            method = Method_Head()
        case "POST":
            method = Method_Post()
        case "PUT":
            method = Method_Put()
        case "DELETE":
            method = Method_Delete()
        case "CONNECT":
            method = Method_Connect()
        case "OPTIONS":
            method = Method_Options()
        case "TRACE":
            method = Method_Trace()
        case "PATCH":
            method = Method_Patch()
        case _:
            method = Method_Other(request.method)
    
    url_parsed = parse.urlparse(request.uri)

    match url_parsed.scheme:
        case "http":
            scheme: Scheme = Scheme_Http()
        case "https":
            scheme = Scheme_Https()
        case "":
            scheme = Scheme_Http()
        case _:
            scheme = Scheme_Other(url_parsed.scheme)

    headers_dict = request.headers

    # Add a `content-length` header if the caller didn't include one, but did
    # specify a body:
    if headers_dict.get('content-length') is None:
        content_length = len(request.body) if request.body is not None else 0
        # Make a copy rather than mutate in place, since the caller might not
        # expect us to mutate it:
        headers_dict = headers_dict.copy()
        headers_dict['content-length'] = str(content_length)

    headers = list(map(
        lambda pair: (pair[0], bytes(pair[1], "utf-8")),
        headers_dict.items()
    ))

    outgoing_request = OutgoingRequest(Fields.from_list(headers))
    outgoing_request.set_method(method)
    outgoing_request.set_scheme(scheme)
    if url_parsed.netloc == '':
        if scheme == "http":
            authority = ":80"
        else:
            authority = ":443"
    else:
        authority = url_parsed.netloc

    outgoing_request.set_authority(authority)

    path_and_query = url_parsed.path
    if url_parsed.query:
        path_and_query += '?' + url_parsed.query
    outgoing_request.set_path_with_query(path_and_query)

    outgoing_body = request.body if request.body is not None else bytearray()
    sink = Sink(outgoing_request.body())
    incoming_response: IncomingResponse = (await asyncio.gather(
        poll_loop.send(outgoing_request),
        send_and_close(sink, outgoing_body)
    ))[0]

    response_body = Stream(incoming_response.consume())
    body = bytearray()
    while True:
        chunk = await response_body.next()
        if chunk is None:
            headers = incoming_response.headers()
            simple_response = Response(
                incoming_response.status(),
                dict(map(
                    lambda pair: (pair[0], str(pair[1], "utf-8")),
                    headers.entries()
                )),
                bytes(body)
            )
            headers.__exit__(None, None, None)
            incoming_response.__exit__(None, None, None)
            return simple_response
        else:
            body += chunk

Classes

class IncomingHandler (*args, **kwargs)
Expand source code
class IncomingHandler(Base):
    """Simplified handler for incoming HTTP requests using blocking, buffered I/O."""

    def handle_request(self, request: Request) -> Response:
        """Handle an incoming HTTP request and return a response or raise an error"""
        raise NotImplementedError

    def handle(self, request: IncomingRequest, response_out: ResponseOutparam):
        method = request.method()

        if isinstance(method, Method_Get):
            method_str = "GET"
        elif isinstance(method, Method_Head):
            method_str = "HEAD"
        elif isinstance(method, Method_Post):
            method_str = "POST"
        elif isinstance(method, Method_Put):
            method_str = "PUT"
        elif isinstance(method, Method_Delete):
            method_str = "DELETE"
        elif isinstance(method, Method_Connect):
            method_str = "CONNECT"
        elif isinstance(method, Method_Options):
            method_str = "OPTIONS"
        elif isinstance(method, Method_Trace):
            method_str = "TRACE"
        elif isinstance(method, Method_Patch):
            method_str = "PATCH"
        elif isinstance(method, Method_Other):
            method_str = method.value
        else:
            raise AssertionError

        request_body = request.consume()
        request_stream = request_body.stream()
        body = bytearray()
        while True:
            try:
                body += request_stream.blocking_read(16 * 1024)
            except Err as e:
                if isinstance(e.value, StreamError_Closed):
                    request_stream.__exit__(None, None, None)
                    IncomingBody.finish(request_body)
                    break
                else:
                    raise e

        request_uri = request.path_with_query()
        if request_uri is None:
            uri = "/"
        else:
            uri = request_uri

        try:
            simple_response = self.handle_request(Request(
                method_str,
                uri,
                dict(map(lambda pair: (pair[0], str(pair[1], "utf-8")), request.headers().entries())),
                bytes(body)
            ))
        except:
            traceback.print_exc()

            response = OutgoingResponse(Fields())
            response.set_status_code(500)
            ResponseOutparam.set(response_out, Ok(response))
            return

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

        response = OutgoingResponse(Fields.from_list(list(map(
            lambda pair: (pair[0], bytes(pair[1], "utf-8")),
            simple_response.headers.items()
        ))))
        response_body = response.body()
        response.set_status_code(simple_response.status)
        ResponseOutparam.set(response_out, Ok(response))
        response_stream = response_body.write()
        if simple_response.body is not None:
            MAX_BLOCKING_WRITE_SIZE = 4096
            offset = 0
            while offset < len(simple_response.body):
                count = min(len(simple_response.body) - offset, MAX_BLOCKING_WRITE_SIZE)
                response_stream.blocking_write_and_flush(simple_response.body[offset:offset+count])
                offset += count
        response_stream.__exit__(None, None, None)
        OutgoingBody.finish(response_body, None)

Simplified handler for incoming HTTP requests using blocking, buffered I/O.

Ancestors

Methods

def handle_request(self,
request: Request) ‑> Response
Expand source code
def handle_request(self, request: Request) -> Response:
    """Handle an incoming HTTP request and return a response or raise an error"""
    raise NotImplementedError

Handle an incoming HTTP request and return a response or raise an error

Inherited members

class Request (method: str, uri: str, headers: MutableMapping[str, str], body: bytes | None)
Expand source code
@dataclass
class Request:
    """An HTTP request"""
    method: str
    uri: str
    headers: MutableMapping[str, str]
    body: Optional[bytes]

An HTTP request

Instance variables

var body : bytes | None
var headers : MutableMapping[str, str]
var method : str
var uri : str
class Response (status: int, headers: MutableMapping[str, str], body: bytes | None)
Expand source code
@dataclass
class Response:
    """An HTTP response"""
    status: int
    headers: MutableMapping[str, str]
    body: Optional[bytes]

An HTTP response

Instance variables

var body : bytes | None
var headers : MutableMapping[str, str]
var status : int