Skip to content

Middleware

Dawid Kraczkowski edited this page Feb 24, 2021 · 3 revisions

Middleware are functions or classes that inherit chocs.Middleware. Middlewares have access to the request object and the next function which can be used to control middleware stack flow. Successful middleware execution should call the next function which accepts a chocs.HttpRequest instance and returns chocs.HttpReponse.

Middlewares can perform various tasks:

  • Making changes in request/response objects ending
  • Validating input data
  • Authenticating users
  • End request-response cycle
  • Connecting to external data sources

Defining and registering middleware function

By default, if something goes wrong in your application and exception is thrown a default 500 response is returned to the client. This behaviour can be changed with custom middleware. The following code defines simple middleware to catch all application's exceptions and returns valid json response to the client.

import chocs
import json
from typing import Callable

# middleware must always accept two parameters; HttpRequest and Callable and return HttpResponse
def handle_errors(request: chocs.HttpRequest, next: Callable) -> chocs.HttpResponse:
    try:
        return next(request) # we pass request further to middleware pipeline
    except Exception as error: # if exception is thrown it is caught here and new response is generated instead
        json_response = {
            "code": error.code,
            "message": str(error),
        }
        return chocs.HttpResponse(json.dumps(json_response), status=500, headers={"content-type": "application/json"})
    
# below code is registering middleware
app = chocs.Application(handle_errors)

@app.get("/hello")
def say_hello(request: chocs.HttpRequest) -> chocs.HttpResponse:
  raise Exception("You will see this message in the response instead")
  return chocs.HttpResponse("this one below")

chocs.serve(app)

Defining and registering middleware class

Now let's explore the same problem with different approach (registering middleware class). This approach might be considered when middleware has to keep its state or its behaviour needs to be parametrised.

import chocs
import json
from typing import Callable

class HandleErrorsMiddleware(chocs.middleware.Middleware):
  def __init__(self, response_code: int = 500):
    self.response_code = response_code
  def handle(self, request: chocs.HttpRequest, next: chocs.middleware.MiddlewareHandler) -> chocs.HttpResponse:
    try:
      return next(request)
    except Exception as error: 
      json_response = {
        "code": error.code,
        "message": str(error),
      }
      # Response will use response_code variable to set status code's value
      return chocs.HttpResponse(json.dumps(json_response), status=self.response_code, headers={"content-type": "application/json"})

# below code is registering middleware
app = chocs.Application(HandleErrorsMiddleware(501))

@app.get("/hello")
def say_hello(request: chocs.HttpRequest) -> chocs.HttpResponse:
  raise NotImplemented()

chocs.serve(app)

In the above example chocs.middleware.Middleware abstract class is being used to extend our middleware. Another difference that can be observed between middleware as function and middleware as a class is the next parameter definition. In the middleware as a class approach we have to match the interface of chocs.middelware.Middleware abstract class, but in both scenarios it is the same Callable object.