Request Handlers

There are two main types of request handlers that whirlwind provide, Simple and SimpleWebSocketBase for http and websockets respectively. There’s nothing that forces you to use these classes, but they do provide some benefits.

Simple Request Handler

To use this class you do something like:

from whirlwind.request_handlers.base import Simple

class MyRequestHandler(Simple):
    async def do_get(self):
        return {"hello": "world"}

    async def do_put(self):
        body = self.body_as_json()
        return {"echo": body}

Simple will use do_get, do_put, do_post, do_patch and do_delete for each of the HTTP methods. If the handler receives a method that isn’t implemented it will return a 405 response. These hooks are all async functions.

If the method returns a string that starts with <html> or <!DOCTYPE html> then it returns the response with a Content-Type of text/html. If the result is a dict or list then it will turn the result into a JSON string and provide the Content-Type as application/json; charset=UTF-8.

Otherwise it will just write the return to the response and provide the Content-Type as text/plain; charset=UTF-8.

Converting objects to JSON

When converting the result to a JSON object it will convert the object such that byte objects are converted using binascii.hexlify(obj) and anything else that isn’t JSON serializable is converted using repr(obj). You can modify this behaviour by setting a different reprer on the handler. For example:

class Thing:
    def __str__(self):
        return "thing as a string"

class MyRequstHandler(Simple):
    def initialize(self, thing):
        super().initialize()
        self.thing = thing

        def other_reprer(o):
            """Convert non json'able objects into strings"""
            return str(o)
        self.reprer = other_reprer

    async def do_get(self):
        return {"thing": self.thing}

class Server(Server):
    def tornado_routes(self):
        return [("/one", MyRequestHandler, {"thing": Thing()})]

await Server(asyncio.Future()).serve("0.0.0.0", 9001)

# curl http://0.0.0.0:9001/one will return {"thing": "thing as a string"}

Converting exceptions to messages

The other thing that these handlers do is convert exceptions into messages for the response. It will use the message_from_exc function on the handler to convert the exception to a message and then the conversion rules for a normal returned object from the handler apply to this message.

By default message_from_exc will treat whirlwind.request_handlers.base.Finished exceptions as a special case and return an InternalServerError for everything else.

For example:

from whirlwind.request_handlers.base import Finished, Simple
from whirlwind.server import Server

class Handler1(Simple):
    async def do_get(self):
        raise Finished(status=400, detail="information")

class Handler2(Simple):
    async def do_get(self):
        raise ValueError("Bad")

class Server(Server):
    def tornado_routes(self):
        return [
              ("/one", handler1)
            , ("/two", Handler2)
            ]

# curl /one returns a 400 response that says
# {"status": 400, "detail": "information"}

# curl /two returns a 500 response that says
# {"status": 500, "error_code": "InternalServerError", "error": "Internal Server Error"}

If you want to modify how exceptions are turned into messages then you give the handler a new message_from_exc callable. This is a function that takes in exception_type, exception, traceback, which is the information you get from calling sys.exc_info().

If you want to keep the existing behaviour, then you can subclass the whirlwind.request_handlers.base.MessageFromExc class. For example:

from whirlwind.request_handlers.base import Finished, Simple, MessageFromExc
from whirlwind.server import Server

class WhoAreYou(Exception):
    pass

class MyMessageFromExc(MessageFromExc):
    def process(self, exc_type, exc, tb):
        """This hook is used if the exception is not a Finished exception"""
        if isinstance(exc_type, WhoAreYou):
            return {"status": "401", "error_code": exc_type.__name__, "error": "Couldn't identify you"}
        return super().process(exc_type, exc, tb)

class Handler(Simple):
    def initialize(self):
        super().initialize()
        self.message_from_exc = MyMessageFromExc()

    async def do_get(self):
        raise WhoAreYou()

class Server(Server):
    def tornado_routes(self):
        return [("/one", handler)]

# curl /one returns a 401 response that says
# {"status": 401, "error_code": "WhoAreYou", "error": "Couldn't identify you"}

Websocket Handler

The other request handler type is the SimpleWebSocketBase which lets you create a websocket handler. For example:

from whirlwind.request_handlers.base import SimpleWebSocketBase
from whirlwind.server import Server

import time

class WSHandler(SimpleWebSocketBase):
    async def process_message(self, path, body, message_id, message_key, progress_cb):
        progress_cb({"called_path": path, "called_body": body})
        return {"success": True}

class Server(Server):
    async def setup(self):
        self.wsconnections = {}

    def tornado_routes(self):
        return [
              ( "/ws"
              , WSHandler
              , {"server_time": time.time(), "wsconnections": self.wsconnections}
              )
            ]

    async def cleanup(self):
        # Wait for our websockets to finish
        ts = list(self.wsconnections.values())
        if ts:
            await asyncio.wait(ts)

# Opening the websocket stream to /ws will get us back this message
# {"reply": <the server_time>, "message_id": "__server_time__"}
# unless you supply server_time as None, in which case it won't send server_time

# Then when we send the message {"path": "/somewhere, "body": {"something": True}, "message_id": "message1"}
# We get back the following two messages
# {"message_id": "message1", "reply": {"progress": {"called_path": "/somewhere", "called_body": {"something": True}}}
# {"message_id": "message1", "reply": {"success": True}}

Everything about how the replies and exceptions are treated (and the reprer and message_from_exc functions) are the same for the websocket handler.

The handler is opinionated however and will complain if your messages are not of the form {"path": <string>, "body": <value>, "message_id": <string>}. Also all replies are of the form {"message_id": <message_id from request>, "reply": <object>}

When you call the progress_cb callback the reply will be of the form {"message_id": <message_id_from-request>, "reply": {"progress": <object given to progress_cb}}

Also, the Websocket handler takes in server_time and wsconnections as parameters. The server_time is used to tell the client the time at which the server was started. This is so the client can determine if the server was changed since the last time it started a websocket stream with the server. If you supply server_time as None then it won’t send this message.

The wsconnections object is used to store the asyncio tasks that are created for each websocket message that is received. It is up to you to wait on these tasks when the server is finished to ensure they finish cleanly.

The handler will create a unique uuid for every message it receives and use that as the key in wsconnections. This unique uuid is passed into process_message as message_key.

The other thing that this handler will do for you is handle any message of the form {"path": "__tick__", "message_id": "__tick__"} with the reply of {"message_id": "__tick__", "reply": {"ok": "thankyou"}}. This is so clients can keep the connection alive by sending such messages every so often.

Progress Callback

You can intercept calls to the progress_cb by implementing transform_progress on your handler. For example:

from whirlwind.request_handlers.base import SimpleWebSocketBase

class WSHandler(SimpleWebSocketBase):
    def transform_progress(self, body, progress, **kwargs):
        # Body will be the whole message.
        # i.e. ``{"path": "/one/two", "body": {"arg": 1}, "message_id": <message_id>}``
        # progress will be the first argument to progress_cb
        # kwargs is any keyword arguments given to progress_cb
        # You then yield 0 or more messages that will be sent back
        if progress == "ignore":
            # Note that you must yield somewhere in the function so it's a generator function
            # Even if you never return from it
            return

        if type(progress) is list:
            for thing in progress:
                yield {"progress": progress, "kwargs": kwargs}
        else:
            yield {"progress": progress}

    async def process_message(self, path, body, message_id, message_key, progress_cb):
        # With transform_progress above this will generate no progress reply
        progress_cb("ignore")

        # This will generate multiple progress replies
        progress_cb([1, 2], arg=3)

        # This will generate one progress message
        progress_cb({"called_path": path, "called_body": body})

        return {"success": True}

By default transform_progress will ignore all keyword arguments and just yield the progress argument once.

Response message for a Websocket Handler

SimpleWebSocketBase provides a hook that is called when the process_message method finishes and has sent the reply back to the client. This hook takes in the original request, the final message (after transformations), the message_key uuid generated for this message by the server; and exception information if process_message raised an exception.

For example:

from whirlwind.request_handlers.base import SimpleWebSocketBase

class WSHandler(SimpleWebSocketBase):
    def message_done(self, request, final, message_key, exc_info):
        # For example, if our final message says "closing" we can close the connection
        if type(final) is dict and final.get("closing"):
            self.close()

    async def process_message(self, path, body, message_id, message_key, progress_cb):
        return {"closing": True}

Sending files to an endpoint

You can send files to an endpoint by sending a normal multipart/form-data request. If you specify a __body__ file then when you say self.body_as_json() it will get treat that file as the body of the request.

This is useful for the commander functionality of whirlwind where the body of the command can be specified with the __body__ file.

You can then access the files in your handler by accessing self.request.files

Logging of exceptions

By default the Simple and SimpleWebSocketBase handlers will log exceptions when the request raises an exception. You can prevent this by providing the handler with a log_exceptions = False class attribute:

class Handler(Simple):
    log_exceptions = False

    async def do_get(self):
        raise ValueError("error")