saviorand / lightbug_http

Simple and fast HTTP framework for Mojo! 🔥
MIT License
394 stars 27 forks source link

Propose Middleware Solution #33

Open drujensen opened 2 months ago

drujensen commented 2 months ago

This is a proposal and code sample for adding middleware to the light bug project.

Proposal

The chain of responsibility design pattern is used to chain together pieces of middleware that will each handle some aspect of the Request/Response. This allows someone to choose which middleware they want and allow them to remove or replace anything they need. It also supports adding their own custom middleware.

The Context is used to wrap the HTTPRequest and provided a dictionary of parameters. It should support AnyType of value so you can add middleware to handle authentication or parsing cookies, query strings, paths and provide that to other middleware or handlers.

In the middleware package, I've provided 8 example middleware handlers: Error, Compression, Logger, CORS, Basic Authentication, Static, Router, and NotFound.

The Router middleware is special in that it allows for different leaf nodes to be registered for a particular Request Pattern. This can become complex with routing based on any method, path, header combination. Also, it will need to support variable paths. The one provided is very rudimentary. We will want one that uses a high speed tree structure for the best performance.

The HTTPHandler trait is used by the Router for the leaf nodes. We can add a LambdaHandler and support Lambdas when they become available.

The NotFound is the last item in the chain. If no other middleware handles the Request, it will return a 404, not found response. By not hard coding this in the Router, we can support multiple routers in the chain which provides flexibility for mounting other projects or have Authorized vs Unauthorized routes.

The MiddlewareChain handles creating the linked list of Middleware. I added support for the existing HTTPService trait so it can easily be used instead of the existing services.

The lightbug_http.mojo was modified to demonstrate the use of the Middleware.

I also renamed the lightbug_welcome.html to index.html so the StaticMiddleware could be used.

Current Issues:

  1. Traits are not fully supported yet so the next in the middleware and the root in the chain are failing to compile:

    /middleware/router.mojo:11:5: error: TODO: dynamic traits not supported yet, please use a compile time generic inste
    ad of 'Middleware'
    var next: Middleware
  2. The Dict collection requires the value to support the CollectionElement trait. Both the HTTPHandler and AnyType are traits and not implementations so this causes issues with the Dict in the Router and Dict in the Context that holds the parameters.

    /middleware/router.mojo:12:30: error: 'Dict' parameter #1 has 'CollectionElement' type, but value has type 'AnyTrait
    [HTTPHandler]'
    var routes: Dict[String, HTTPHandler]
saviorand commented 1 month ago

This looks really good! Thank you for this contribution. For me the most important question is how to distribute this functionality between this library (lightbug_http) and potential future libraries like lightbug_api or lightbug_web which are all supposed to build on each other.

I looked at how this is done in other languages. Seems like middleware infrastructure is normally already included in basic libraries like lightbug_http, e.g. Python's Starlette that I'm heavily using as inspiration for lightbug_http includes the middleware class and two implementations: ServerErrorMiddleware and ExceptionMiddleware. Deciding where to put the context part is a bit more challenging. I've noticed at least 2-3 different approaches to this. 1) Starlette does not include context by default in the library, there's a third-party package that can be imported to provide this. 2) In Golang, context is part of the standard library and usually just imported in addition to a framework, which is nice. Not sure if it will be added to Mojo stdlib, at least it is not there currently. 3) In Rust, e.g. Actix, which I think lightbug_http is also in spirit of, does include context in the package, but it also has the concept of actors which carry context, which we probably (?) wouldn't want to explore.

About the technical issues you mentioned:

drujensen commented 1 month ago

Thanks for taking a look at the proposal.

It looks like Mojo will need to mature a bit before we can build out a proper web framework. I'm hoping they provide a standard for creating middleware built-in to the Mojo standard library. The Crystal language did this and it helped with compatibility between frameworks.

Regarding the issues, I think rust like traits, go like interfaces or OO inheritance will be needed for the duck typing to work. Looking forward to when Mojo properly supports this capability.

saviorand commented 1 month ago

Posted a thread on the questions channel in Discord, perhaps the community will have some ideas.

image
saviorand commented 1 month ago

@drujensen artemiogr97 from Discord suggested we do this:

@value
struct RouterMiddleware[HTTPHandlerType: HTTPHandler](Middleware):
    var next: Middleware
    var routes: Dict[String, HTTPHandlerType]

    fn __init__(inout self):
        self.routes = Dict[String, HTTPHandlerType]()

I tried it out, seems to work! Compiler error went away. Guess I should've read Mojo docs more closely 😅

saviorand commented 1 month ago

@drujensen should we continue to pursue this, since duck typing seems to work?

drujensen commented 1 month ago

@savioran I have tried the approach but haven't figured it out yet. I'm a nooby to Mojo and the metaprogramming stuff takes a bit to get used too. The documentation is sparse and copilot is useless. I do have the lsp working in neovim so that should help make the process a bit smoother. I will continue to look into this and see if I can get a working version.