-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enable Routing by Content-Type Header #1654
Comments
I think this is definitely something we could explore inside @davidpdrsn wdyt about axum growing its own |
I think it's worth exploring! |
Looking through the code i can see how this is can be "E-hard" :) Perhaps another option would be to do something like this ?
I think that would only require changes to Node and instead of str we woudl have a tuple (str, mime) |
There are definitely ways to solve this that aren't that hard, but if it's going to be part of axum-extra and not a thirdparty project, I want it to be very close to the existing API and not something else just because that might make the implementation simpler 😉 (and I'm sure David agrees) |
Yeah I agree it should feel very close to the existing API. I also think how axum doesn't allow overlapping routes is an important feature and any additional routing we support should do the same. I have prototyped different solutions in the past but never found anything that was strictly better than just using a match statement inside the handler, or an extractor like https://github.com/tokio-rs/axum/blob/main/examples/parse-body-based-on-content-type/src/main.rs#L54 |
I have been playing around with the routing code and opened a draft pull request 1679 for custom router PoC. I see it more as brainstorming opportunity than merge request so the feedback will be greatly appreciated. Generally, I am trying to make Router generic so I could instantiate a router with that takes anything that implements newly added RouterResolver trait (for example MethodRouter). This should enable us to keep different/future/bespoke RouterResolver implementations in axum-extra. While it works, there are certain issues:
|
I haven't read the code very closely but my impression is that it introduces a lot of new complexity that I don't think is worth while. I'd be more interested to hear what you need
Feels like you're misunderstanding the example. The extractor doesn't have to be an enum. You could also do |
The use case is pretty simple, as mentioned at the top SpringBoot and dotnet hide the content type resolution from the business logic and you can specify that a given handler is only going to accept or produce specific content based on headers (Content-Type, Accept, Host ) from a request. For example in SpringBoot you can do something like this:
The obvious advantage here is that the framework selects an endpoint by method, content-type and accept and other headers and as a developer i don't have to repeat this code in my handlers. The ask here would be to provide such a Router that we don't have to write JsonOrForm(T) or JsonOrTextOrXml() as part of the application and instead we could have something like :
I appreciate that implementing this new functionality will be quite hard. I do think that the complexity stems from the fact that MethodRouter and Router are very tightly coupled. Router resolves the path part and then MethodRouter looks at the HTTP method part, but Router is strongly dependent on MethodRouter. If we wanted to use something like MethodAndContentType router and keep it in axum-extern as suggested by @jplatte then we need to loosen the coupling between Router and MethodRouter. If there is an easier way to achieve the goal that would be great. |
I'm still not convinced that making the routing more complicated is worth it for this use case. Using an extractor to handle different |
Ok, let's close it. I think this is more of how you see Axum evolving in the future. Wheter it is going to stay as a pretty low level library where ultimately the responsibility falls on the user to implment a lot of generally common logic. Or it is going to evolve into something richer and hide the common scenarios from the user. It would be nice to hava a mechanism for extending routing in Axum, but it looks like without breaking changes to Router and MethodRouting I don't think this is possible. |
I think we should keep this issue open as this is a use case that makes sense for axum to support. The hard part is figuring out how to best do that.
There are more axis than high and low level. Minimal and full featured is another. Personally I consider axum to be a high level but minimal framework. By high level I mean the code you write doesn't have to care about low level transport level details and by minimal I mean we're quite conservative with which features are built into the axum crate itself. This is all very subjective and depends on your goals and what you're comparing to. My goal is for axum to provide APIs that people can use to implement whatever it is they need for their app, without having opinions about the exact implementation. For example axum should make it easy to share a database connection pool throughout your app, but shouldn't have an opinion on which database or pooling library you use. |
Perhaps for the purpose of keeping axum minimalistic, routing as a whole should be pulled out of axum. |
@Kinrany something like that has been suggested before but I'm not sure it would provide much value and probably make things harder to use. I think needing routing is pretty common so it makes sense to build in. |
I would argue that some default routing should be present. But at the same time it should be easy to plug in a different routing mechanim without having to change the code in axum core. At the moment the only way to handle content-type is to implement necessary routines in a handler. This sort of leads to a situation where we repeat the same logic potentially in many handlers and sort of creating additional layer of sub handlers.
It doesn't look nice, there is a lot of repetitions. On top of that we esort of need to put all logic into the handler or risk being inefficient pariculalry if we could/should make a routing decision before we need to process the body. An example here would be handling bodies which have been compressed. Another potential use case would be authorization. We would put authorization in a layer/service so the handler can get a principal etc. At the same time, we don't want to execute a costly authorization process if the handler doesn't know what to do with the body. In the existing version we can only make this decision in the handler where ideally we should be returning 406 Not Acceptable at the routing stage. An alternative would be to move the routing by content type logic into the framework itself. And it is a tradeoff where we sacrifice simplicity of the framework and speed for user ergonomics and ease of use. In the absence of data whether people are using endpoints with different mime types, it is hard to justify one approach over the other. What would be nice though is to make routing more pluggable (similar to services/layers/middlewares) so it is easy for end user to plug their own or to select different routing policies based on their intended usage patterns. |
I've been thinking that you can use let app = Router::new().route(
"/",
get(with_content_type::<ApplictionJson, _>(json)
.or(with_content_type::<TextPlain, _>(text))
.or(fallback)),
);
async fn json() { println!("its json!") }
async fn text() { println!("its text!") }
async fn fallback() { println!("fallback") } The code for that is here. @dawid-nowak what do you think? It's not perfect but maybe it's better than having to roll your own? We could put it in axum-extra so users can try it out. |
It is a tradeoff but this approach could work. And it does remove the boilerplate. I suppose my question would be whether you can use all other Axum goodies with this approach. For example not sure the below would work but I am guessing that is more the property of or operator. get(with_content_type::<ApplictionJson, _>(json)
.or(with_content_type::<TextPlain, _>(
text.layer(ConcurrencyLimitLayer::new(64))
))
.or(fallback)) |
I don't believe you can add middleware to handlers in an "or" because reasons :P you need to know which extractors something needs to know if one of them reject but middleware require the whole request. Is that something you can work around, probably. |
Actually, there is another use case that sort of breaks it from the usability perspective :( let app = Router::new().route(
"/",
get(with_content_type::<ApplictionJson, _>(json)
.or(with_content_type::<TextPlain, _>(text))
.or(fallback)).route_layer(ValidateRequestHeaderLayer::accept("application/json"))
); My understanding is, that In this case, we will validate for accept header and then route to json or text handler. But since the endpoint could be processing either text or json, I would expect that accept header could be either text or json. It looks like the way to solve the problem would be to add another helper with_content_type_and_accept or perhaps more even more future proof it and provide something like with_headers(Map<Header, Vec>). But even with this approach, things are suboptimal if we start using compression or authorization layers (or any other layer that takes long to execute before the handler). In the case below, we are trying to de-compress payload even if we could infer at a routing phase that there is no handler do so: post(with_content_type::<ApplictionJson, _>(json)
.or(with_content_type::<TextPlain, _>(text))
.or(fallback)).route_layer(RequestDecompressionLayer::new()) Would something like this perhaps work ? let handler_builder = HandlerBuilder::new();
handler_builder = handler_builder.add_handler(json).with_content_type(ApplictionJson).with_accept(ApplicationJson).with_decompression().with_ccompression().with_authorization();
handler_builder = handler_builder.add_handler(text).with_content_type(TextPlain).with_accept(TextPlain).with_request_decompression().with_response_compression().with_authorization();
post(handler_builder.build()) |
Well yes of course, if you wanna check multiple headers then you gotta do that. I just didn't do that in my example but the exact same technique works.
I don't see the problem. Compressing "nothing" is basically free.
You could make such an api I think so yes. |
We have a post with payload, we are going to de-compress the payload, and then we are going to realize that we have no handler to handle the content-type. Does it sound plausible or am i missing something ? |
Ah sorry. I thought you meant compress the response. Decompression should work fine as well. Decompression doesn't do anything unless you actually read the body. It doesn't eagerly decompress it. |
Ahh, cool. post(json).route_layer(RequestDecompressionLayer::new()).route_layer(CompressionLayer::new()) |
Nope the order shouldn't matter. You can also use a tuple of layers (that's a layer as well)
|
@dawid-nowak Why not have the handlers take an already-deserialised form of the request body regardless of content type, and put content type specific deserialisation code in an extractor? This way the code will live in only one place. |
Yes, please! ❤️ ... Actually, I'd want this for the |
I also ran into this problem today. The client may initiate different content-types, and I need to decide whether to make a streaming response or a simple json response based on this content-type. It seems impossible to accomplish this task with the same handler? |
You can extract the headers with |
For my case, I implemented |
I'd argue that exposing what the server accepts is important for hypermedia scenarios or other scenarios (as described here). For a valid router to chose the correct handler based on complex If you let handlers take care of this, will introspection still be possible to help generators like https://github.com/jakobhellermann/axum_openapi? It could make sense to let the axum router handle this complexity. The same applies to other kinds of server-side negotiations, like |
Support for that could be built into the openapi library. I'm not sure this means it needs to be built into axum and even if it was the openapi lib would have to have code to handle such routes. |
Starting from An example of the latter could be to allow versioning of an API through an "API Version" header: C# / .NET, for example, supports Interestingly, this exact difference is discussed at length wrt. how to best version REST APIs, e.g. in this StackOverflow discussion, which ends up making similar points about versioning the "API Resource" versus individual responses. Several organizations use headers to manage their API Versions and hence their responses and endpoints in practice. One of the most prominent examples I know of is GitHub, which uses a custom In contrast to resource-specific changes like encoding, the routing of a web application with multiple top-level API versions could be entirely different between those versions: endpoints can be removed and new ones added from one version to another, so if the place to handle this in a program are the handlers, then a handler has to exist for any possible route in any version which checks the header and returns a 404 for requests with unsupported versions. Putting aside that that already sounds like what routing is for, this means that you cannot define a V1 API and a V2 API separately (in the code), but have to make that distinction inside each handler, i.e., adding or removing version of the That said, I think it should be possible to implement this in the same way that you suggest in #1654 (comment) plus a handler that rejects present but invalid API versions before delegating to the fallback. Though you'd probably want this with |
What I would want and expect from a router is automatic handling of invalid requests from configured metadata. For example, if It's also worth pointing out that GET /resource HTTP/2
Host: example.api.com
Accept: application/xml
Content-Type: application/json
Content-Length: 42
{"message":"Hello"} It gets more complex with quality values in In my experience, most service authors tend to prefer well-behaved HTTP semantics which are configured declaratively (e.g consumes and produces). |
Total sidebar because I did not expect to see references here when I stumbled upon this issue, but I am the author of API Versioning for the .NET stack. API versioning is an interesting topic. I'm also a fellow 🦀 and I'm certainly interesting in bring the concept to Rust, which I believe would manifest in Today, it seems that implementing versioning is very limited. Option one would be versioning within a URL segment (e.g. I don't have specific suggestions or ideas - yet, but I can share that the original routing system for ASP.NET Core had a lot of challenges when it came to API versioning. Versioning was one of the key use cases to vet the redesigned/rewritten routing system 5 years ago. I already see limitations in I don't want to detract any further from this issue. If this is interesting to someone (@domenicquirl, @dawid-nowak?), feel free to ping me over email to continue the discussion or provide link for collaboration. Happy to share any thoughts or opinions that are on-topic to routing here. When I eventually wrap my head around everything and hit a wall, I'll be sure to open a new, appropriate issue for discussion. |
I find these 2 quotes interesting because it shows how arbitrary what is deemed valid as "routing".
Currently in axum, the same resource can already be handled by multiple handlers (one for each verb f.e), so why should it be different for representations? It all depends on how much of the handler logic is generic over verb (unlikely), representation (depends), or version (maybe). Representations could just be changing the serialization of the same struct (like JSON vs YAML), but for HTML it could very likely necessitate a different handler, because the representation is so different and includes so many more things. In any case, what matters is not whether a resource is backed by 1 or many handlers, but more: "how easy it becomes when axum helps me "route to 1 of multiple handlers" or "pass info to the single handler" based on the The proposition for now has always been to do 100% of the work yourself, by extracting the preferred representation yourself, and doing the internal "routing" (or whatever you want to call it) yourself. But my argument is that it's arbitrary to decide that paths and verbs are the ONLY valid informations worth having special behavior for. If I take the same argument for http verbs, then "use an extractor and inject the http verb in the handler and cascade-fail until you find the one correct handler" is equally valid and could be the response to everything. What I'm interested in is the ability to do content negotiation. If there is a good story for that that doesn't involve modifying axum then great, but in the meantime there is no proper story. EDIT: I now realize that I'm talking about the generalized "route by anything" discussion above. |
I still believe what I suggested here is worth exploring in a separate crate. |
I solved this issue in my project by putting routes which need a specific header under a prefix path, and then adding a middleware to rewrite routes based on the header. Here you can see the code. |
I'd like to add that a generalized solution could be also useful for altering the API behavior based on request headers (in my specific case this is basically used for versioning an API, yes, I know it's more common to use a separate URL, but in my use case this is more convenient), which is what I wanted and why I ended up finding this issue. I don't know why routing should be only constrained to URLs, it seems arbitrary. |
In my use case, the mime type is just for rendering so that can be done with the usual Axum mechanisms. However, the version is extremely important. The fields will change between versions and possibly semantics for the fields common between versions. I considered the middleware solution above but this won't help in my case. The client isn't expected to know what a given request will return so it simply sends all the mime types it knows how to handle (e.g. application/vnd.myapp.book+json; version=2, application/vnd.myapp.author+json; version=1). So the middleware can't simple check what version is in use since these will vary between resources. The middle ware would have to check the path to see what resource is being requested and check a map to see which route to use for that version. But this would be full routing at that point. I suppose for the time being I can try to use a macro to generate the e.g. |
This comment was marked as off-topic.
This comment was marked as off-topic.
@jason-johnson Please open a separate discussion about the specific thing you're trying to achieve currently, and ideally add some code snippets (I think that would make it easier to follow your explanation of the problem). |
Done: #2888 |
Feature Request
Enable Content Based Routing
Motivation
Currently Axum routes by path only so in order to handle different types of content an approch shown in example has to be implemented in all handlers resulting in extra boilerplate and complexity that could be handled by the framework.
Other frameworks can route by path and content type. See examples for dotnet and springboot
Proposal
One approach would be to extend the existing Router API to allow the user to pass the content type that the handler is processing.
For example:
Alternatives
Create a completely separate ContentRouter service
The text was updated successfully, but these errors were encountered: