Tutorial: Adding Values To Requests

Requests are preloaded in Eynnyd with all the values from the raw WSGI request. However, as you are processing your request, you may wish to add additional details to it. For example, on routes secured by a session, you may want to load that session from your database in an request interceptor and put that loaded value onto your request for later use (without reloading it). Because python allows you to manipulate objects after they have been built, you could do this simply by doing request.session = session_dao.get_session(request.headers["session"]) but that wouldn’t be very explicit and mutation like this can lead to hidden bugs, surprised readers, and much more. A more explicit way of doing this is shown below.

Some might argue that the mutation method we just talked about is more “pythonic” than the explicit version we are about to show you, however we would direct them to the zen of python (type import this into any python terminal) which specifically (and correctly) states: “Explicit is better than implicit”.

As a simple example, let’s assume we want to add a random ID to every request so that when we log things about it the ID can be matched up.

This tutorial builds on the response interceptors tutorial so if you have not read that yet, and you find something confusing in here, it is recommended you look there for your answer. First we will show you the code and then we will explain the relevant parts (AKA the parts not in the prior tutorials).

# hello_world_app.py
import logging

from eynnyd import AbstractRequest
from eynnyd import RoutesBuilder
from eynnyd import EynnydWebappBuilder
from eynnyd import ResponseBuilder
from eynnyd import ErrorHandlersBuilder

from http import HTTPStatus
import uuid

LOG = logging.getLogger("hello_world_app")

class IDEnhancedRequest(AbstractRequest):

    def __init__(self, original_request, request_id):
        self._request_id = request_id
        self._original_request = original_request

    @property
    def request_id(self):
        return self._request_id

    @property
    def http_method(self):
        return self._original_request.http_method

    @property
    def request_uri(self):
        return self._original_request.request_uri

    @property
    def forwarded_request_uri(self):
        return self._original_request.forwarded_request_uri

    @property
    def headers(self):
        return self._original_request.headers

    @property
    def client_ip_address(self):
        return self._original_request.client_ip_address

    @property
    def cookies(self):
        return self._original_request.cookies

    @property
    def query_parameters(self):
        return self._original_request.query_parameters

    @property
    def path_parameters(self):
        return self._original_request.path_parameters

    @property
    def byte_body(self):
        return self._original_request.byte_body

    @property
    def utf8_body(self):
        return self._original_request.utf8_body

    def __str__(self):
        return "[{i}]<{m} {p}>".format(i=self._request_id, m=self.http_method, p=self.request_uri)

def hello_world(request):
    return ResponseBuilder() \
        .set_status(HTTPStatus.OK) \
        .set_utf8_body("Hello World")\
        .build()

def add_id_to_request(request):
    return IDEnhancedRequest(request, uuid.uuid4())

def log_request(request):
    LOG.info("Got Request: {r}".format(r=request))
    return request

def log_response(request, response):
    LOG.info("Built Response: {s} for Request: {r}".format(s=response, r=request))
    return response

def build_application():
    routes = \
        RoutesBuilder() \
            .add_request_interceptor("/", add_id_to_request) \
            .add_request_interceptor("/", log_request) \
            .add_handler("GET", "/hello", hello_world) \
            .add_response_interceptor("/", log_response)\
            .build()

    return EynnydWebappBuilder() \
            .set_routes(routes) \
            .build()

application = build_application()

New Imports

For our new work we need two new imports.

from eynnyd import AbstractRequest
...
import uuid

The AbstractRequest represents all the functionality a request must contain (at minimum). These values are already provided to your code, loaded from the WSGI server. We are also using the uuid module here to generate us random IDs. Collisions this way are pretty uncommon and since these IDs are short lived (only for the duration of a request) we feel this method is pretty reasonable.

Building An Explicit Request Wrapper Class

We now build a class which implements our Eynnyd AbstractRequest from above explicitly and it also provides us with a request_id property.

class IDEnhancedRequest(AbstractRequest):

    def __init__(self, original_request, request_id):
        self._request_id = request_id
        self._original_request = original_request

    @property
    def request_id(self):
        return self._request_id

    @property
    def http_method(self):
        return self._original_request.http_method

    @property
    def request_uri(self):
        return self._original_request.request_uri

    @property
    def forwarded_request_uri(self):
        return self._original_request.forwarded_request_uri

    @property
    def headers(self):
        return self._original_request.headers

    @property
    def client_ip_address(self):
        return self._original_request.client_ip_address

    @property
    def cookies(self):
        return self._original_request.cookies

    @property
    def query_parameters(self):
        return self._original_request.query_parameters

    @property
    def path_parameters(self):
        return self._original_request.path_parameters

    @property
    def byte_body(self):
        return self._original_request.byte_body

    @property
    def utf8_body(self):
        return self._original_request.utf8_body

    def __str__(self):
        return "[{i}]<{m} {p}>".format(i=self._request_id, m=self.http_method, p=self.request_uri)

There are 3 unique things to note about this class. The first is that every property except the request_id property just returns the value from the original request object. The second note-worthy item is the request_id property itself, which just returns any id set in the constructor. And finally, it is also worth noting we have updated our __str__ method, which means that any logging of this request will now start with a prefixed request id value.

Updating the Request

Now we just need a request interceptor to update our incoming request with new values.

def add_id_to_request(request):
    return IDEnhancedRequest(request, uuid.uuid4())

Because IDEnhancedRequest extends AbstractRequest this code is legal (wont fail Eynnyd’s request interceptor validation). All we are doing is returning the wrapped request with a newly added, random id.

Adding the Interceptor

Finally, we just need to add this interceptor to our routes at the root level and make sure it runs before all other interceptors.

def build_application():
    routes = \
        RoutesBuilder() \
            .add_request_interceptor("/", add_id_to_request) \
            .add_request_interceptor("/", log_request) \
            .add_handler("GET", "/hello", hello_world) \
            .add_response_interceptor("/", log_response)\
            .build()

Note we added this interceptor before the other root interceptors to insure it runs first. With this change both the log_request request interceptor and the log_response response interceptor will log out the request including our new id value.