rwf2 / Rocket

A web framework for Rust.
https://rocket.rs
Other
24.22k stars 1.56k forks source link

Middleware #55

Closed theduke closed 7 years ago

theduke commented 7 years ago

Pretty much every framework out there supports some concept of a middleware.

(Possibly mutating) actions on the request, that run before and after the actual handler.

Useful for all kinds of things, including authentication, authorization, logging, profiling, etc.

This should really be implemented in some form.

cgag commented 7 years ago

I haven't played with rocket yet, and the guard system looks cool but I'm particularly interested in logging and request timing and unsure how to go about doing those things. My current plan would be to write wrapper functions and manually wrap all my routes.

gsquire commented 7 years ago

Some inspiration for what middleware is commonly like can be seen on Iron's README.

Also even though it is written about Go, this article describes the motivation behind middleware in a web framework.

dpc commented 7 years ago

Since the whole handler is just a function call, all middleware functionality require is really being able to conveniently wrap the request function in another function, that could do some pre-work, call the actual handler, then do some post-work.

Seems to me that integrating some common state that can be associated to a given instance of a rocket http server and used in the handlers should also be mentioned.

Here's the usecase to make a good example.

Let's say I want to use slog for structured logging in the request. I build a slog::Logger with server information like port, ip, build id etc. Now in the handlers I'd like to created a child Logger with some more values (eg. IP of the peer), log at least start and end of handling the request. Somehow I need to access this custom user-data from the handler (and it's middleware-like wrapper) to be able to do so.

zweizeichen commented 7 years ago

Maybe consider the design of Elixir's plug. It is a very nice way to deal with such things:

Basically it just hands down a request/response data structure a function pipeline which in turn reads/writes to the structure (JSON parsers, auth systems...), thus providing a very simple and composable API. Certain endpoints can have different pipelines (think /app which serves HTML vs. /api which serves JSON). Due to the way its implemented, it yields very high performance. I would imagine, since Rust offers some nice zero-cost abstractions and the pipelines would be defined statically during compile time, that this would allow for some nice compile-time optimisations.

SergioBenitez commented 7 years ago

Middleware is a concept that I see as overused and abused. It feels like a crutch and catch-all for things that don't fit into a framework's model. It's difficult to work with, and more importantly, hard to follow and understand. Rocket will not support middleware in the customary sense. There must be a better way, and I hope we discover it.

I believe there is a category of problems for which something like middleware, but much, much lighter, is a good fit. Let's call these undiscovered, light-weight things "fairings".

Fairings should be able to handle situations for which request guards, data handlers, parameter handlers, and responders are a poor fit. Rocket's been in the public eye for about a month, and I think we've discovered a few cases for which fairings would be a good fit:

  1. CORS
  2. HTTP Caching
  3. Request/response timing/profiling
  4. Security mechanisms like CSRF

The following are things typically handled with middleware that I believe Rocket's primitives handle in a superior fashion:

  1. Data handling/parsing/validation (via data handlers)
  2. Policy and authentication (via request guards and forwarding)
  3. Asset rendering (via responders)

One common use for middleware is to pass pre-processing information to request handlers. Interestingly, none of the instances identified have a need to pass information. The CORS fairing either short-circuits request handling or adds headers to the response. The HTTP Caching fairing would do the same. The timing firing doesn't modify the request/response structures or path at all but needs a mechanism to retrieve information from the pre-processing side. Indeed, I'd like to find a solution that maintains this invariant: information should never directly flow from pre-processors to request handlers.

To the community: what would you like to do with fairings? What other use cases are Rocket's existing primitives a poor fit for? Let's work together to find a better solution.

mehcode commented 7 years ago

Not sure I agree on premise (middleware is bad) but I'll talk through the use cases. I'm going to use the word middleware below because I'm more comfortable with that word.


Indeed, I'd like to find a solution that maintains this invariant: information should never directly flow from pre-processors to request handlers.

Consider altering the method before the request is handled (eg. X-HTTP-Method-Override or ?_method=); this would need to modify the request.

Consider also altering the request URL to normalize, for instance, /user and /user/.

Those two things could be hard-coded into Rocket as configurable areas.


One common use for middleware is to pass pre-processing information to request handlers.

I don't understand your mental model here. Middleware that does this I can agree is badly written and this kind of thing doesn't exist in any framework I've worked with/on.

Edit: I should clarify here. I mean that if you take away the ability for middleware to be able to send arbitrary data to a request.. what is left that you dislike? I'd also argue that the ability to do so is not a property of middleware in general but rather the request or context object used in conjunction.


Middleware is a concept that I see as overused and abused. It feels like a crutch and catch-all for things that don't fit into a framework's model.

I feel I need to stress that is kind of the point of middleware. The core idea is to extend the system in way the framework doesn't understand (and shouldn't have to understand).


Really what a general middleware system gives us is flexibility.

I'm not understand your dislike of middleware or (again) even what mental model of middleware you're coming from. There are a few I can think of that are all quite different. Can you list what you don't like about middleware and we can discuss from there?

SergioBenitez commented 7 years ago

@mehcode I seem to have struck a chord with you. My apologies if I did! Certainly not my intention!

My "mental model" is surely no different than yours. I wouldn't even go as far as to call it a "mental model"; it's simply what middleware is: code that sits before and/or after a request/response and optionally modifies the request/response and/or adds information to the request for later use. My concept of middleware is based on its implementation in frameworks such as Rails (through Rack), Django, and Laravel.

My main objection to the general idea is that middleware tends to hide the flow of a request. One of the core philosophies guiding Rocket is to always be aware of how a request will be handled. In particular, I want to always be able to answer the question: "What needs to be true for this request to be handled successfully?" I feel that Rocket presently does a great job of allowing one to answer that question, and I fear that middleware may muddle that clarity.

Consider altering the method before the request is handled (eg. X-HTTP-Method-Override or ?_method=); this would need to modify the request.

I never stated that modifying the request is something Rocket shouldn't allow. In fact, I believe it should, and further, that it's necessary for exactly these cases. I tried to be very explicit in qualifying with directly when I said: "information should never directly flow from pre-processors to request handlers". What you've illustrated is an example of indirect information; the request handler has no idea, nor does it need to know, that the request method has been changed.

I don't understand your mental model here. Middleware that does this I can agree is badly written and this kind of thing doesn't exist in any framework I've worked with/on.

Just about every implementation of middleware I've encountered allow this, and most use this feature for many core tasks. For an example, see Rail's list of default middleware.

I'd also argue that the ability to do so is not a property of middleware in general but rather the request or context object used in conjunction.

The "request/context" object is just about the only means by which pre/post processors function, so I'm a bit confused with this statement. And, again, most middleware implementations do allow you to pass arbitrary information to request handlers. Even the Play framework allows this!

Perhaps my clarifications here will allow you to read my original response in a different light. I look forward to any additional use cases you might have.

mehcode commented 7 years ago

I seem to have struck a cord with you. My apologies if I did! Certainly not my intention!

@SergioBenitez Not at all! I apologize if I seem harsh/rough.. I tend to get very passionate about this sort of stuff.


What you've illustrated is an example of indirect information; the request handler has no idea, nor does it need to know, that the request method has been changed.

The "request/context" object is just about the only means by which pre/post processors function, so I'm a bit confused with this statement.

Apologies on how I explained it here. I mean..

This is the end of my experience.

Because Rust is a static language, middleware can't just add some data to the Request unless we expose a generic get/set system like Go web frameworks do.


My main objection to the general idea is that middleware tends to hide the flow of a request. One of the core philosophies guiding Rocket is to always be aware of how a request will be handled. In particular, I want to always be able to answer the question: "What needs to be true for this request to be handled successfully?" I feel that Rocket presently does a great job of allowing one to answer that question, and I fear that middleware may muddle that clarity.

Fair enough. You can't just look at a request and understand exactly what a request needs if there is a middleware looking at a header and rejecting the request.

I don't think we can get around this.. de-centralized flow.. though. We're trying to add some generic bit of code that happens at the start of every single request and, for example, sets a request header. The key bit here is I don't want requests to know about this (eg. profiling) because it needs to be decoupled and perhaps only added during benchmarking, development, or testing.

I'm thinking that the reason you feel this way is that during development of a web service you've encountered the "why is my request not hitting my handler/route?!" and spending X hours debugging only to find its a middleware with a typo on the header its checking.

I'd feel that clear, concise debug logging (which is very possible if we structure the middleware right) of why, when, and where rejections are coming from would go a long way to making this feel more natural.

debug: request rejected from `CORS` on line 215: "Request missing an Origin header"
SergioBenitez commented 7 years ago

Because Rust is a static language, middleware can't just add some data to the Request unless we expose a generic get/set system like Go web frameworks do.

This is what Iron does, for instance. I'd really like to avoid it.

Fair enough. You can't just look at a request and understand exactly what a request needs if there is a middleware looking at a header and rejecting the request. [...] I don't think we can get around this.. de-centralized flow.. though.

What I'm advocating for is a system ("fairings") that makes it so that this "decentralized flow" is opaque to request handlers. A request handler doesn't know when it will run, so blocking requests is opaque to it. It doesn't know the headers, method, etc. of the original request, so changing these things, too, is an opaque action.

I'd feel that clear, concise debug logging (which is very possible if we structure the middleware right) of why, when, and where rejections are coming from would go a long way to making this feel more natural.

I agree wholeheartedly. Rocket takes great care to log the flow of a request at present. The introduction of fairings shouldn't hinder this.

To reiterate, the input that I feel is crucial here is: Which use cases are Rocket's existing primitives a poor fit for? Do any of these use cases require arbitrary information to be passed to request handlers?

3n-mb commented 7 years ago

@mehcode @SergioBenitez

Rocket, for example, can parse body of http request as json, when a particular attribute type and annotations are used. In express (js), I would use a middleware to do this parsing.

Is it then fair to say that some common middleware functionality is absorbed into rocket's prebuilt machinery?

Sometimes you want to check header of the request, and reply back without even trying to parse body. Can this be done now in rocket? With middleware we put layer-by-layer, i.e. developer of an app has a strict control of operations in a pipeline. Can rocket give this control, or is it a tool sharpened for some default pipeline shape?

On another note, what rocket does with types is super-cool, and is a magic that is worth it. Generic get/set system is so ancient, that ... . Ya :) .

Netty uses pipeline concept. May it better be adopted, in a rusty way. For a particular pipeline we compose f1(r: t1) -> t2 and f2(r: t2) -> t3, into pipeline!(f1, f2). Compiler/macro magic should match input/output types, failing at compile if given functions have incompatible types. If f1 sends reply directly, other functions can be skipped (return something like option.none?). Pipelines can also be joined.

3n-mb commented 7 years ago

@SergioBenitez , how do you mount rocket app inside of bigger rocket app on some route?

kestred commented 7 years ago

@SergioBenitez

I definitely love the ways the Rocket currently removes the need for middleware, including ManagedState and RequestGuards which are both powerful and effective tools. (There is possible use for a RequestGuard that doesn't provide a value?).


The case where Rocket's current tool-set doesn't work well for me is "request/response timing/profiling" which might then be collected with rust-metrics or prometheus.

My fast and lazy approach to handle metrics was to write code like:

#[post("/stuff")]
fn post_stuff() -> Result<Redirect, Failure> {
    let metrics = collect_metrics!("post", "/stuff");

    /*  ... do stuff ... */

    metrics.finish(Status::SeeOther);
    return Ok(Redirect::to(&target));
}

Some apparent flaws to this hack include that it doesn't capture render time; it doesn't capture routing time; it has boilerplate that's easy to mistype; and it doesn't behave well with exceptions/panics.

I'm confident if I spend more time I could mitigate some of those issues by implementing a RequestGuard to start the data collection, a Response wrapper which uses the guard, and possibly codegen to hide the extra boilerplate; however, it would still be an incomplete solution.


As an extension to this, if someone wanted to use a tracing system like OpenTracing or Zipkin, there could be a use case for timing (or lifecycle hooks :/ ) in each part of the request lifecycle to be able to track and measure each portion of a single request (maybe data handling, request guards, or forwarding take time or perform complex checks).


Overall, I'm advocating against Rails-like Middleware, but am still searching for a good solution to collecting metrics.

SergioBenitez commented 7 years ago

Fairings have landed in https://github.com/SergioBenitez/Rocket/commit/ac0c78a0cd52042237cd5bb085548b0ae64bdb40! All of the use-cases we've mentioned, and many we haven't, should be easily implementable on-top of fairings. Looking forward to experience reports! The fairings example contains a simple illustration on how to use them.