Skip to content
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

Open
dawid-nowak opened this issue Dec 19, 2022 · 41 comments
Open

Enable Routing by Content-Type Header #1654

dawid-nowak opened this issue Dec 19, 2022 · 41 comments
Labels
A-axum-extra C-feature-request Category: A feature request, i.e: not implemented / a PR. E-hard Call for participation: Experience needed to fix: Hard / a lot

Comments

@dawid-nowak
Copy link

  • [v ] I have looked for existing issues (including closed) about this

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:

let app = Router::new()
    .route("/path", post(path_handler_for_json, "application/json")
    .route("/path", post(path_handler_for_xml,"application/xml")

Alternatives

Create a completely separate ContentRouter service

@jplatte
Copy link
Member

jplatte commented Dec 19, 2022

I think this is definitely something we could explore inside axum-extra. However, that would require first making .route more generic.

@davidpdrsn wdyt about axum growing its own Service & Layer trait equivalents with blanket implementations for any type implementing the tower ones? These could be generic over the state type so we can get rid of the {route,nest,..} vs. {route,nest,..}_service distinction in the public API again. Then axum-extra could provide MethodAndContentTypeRouter.

@jplatte jplatte added C-feature-request Category: A feature request, i.e: not implemented / a PR. E-hard Call for participation: Experience needed to fix: Hard / a lot A-axum-extra labels Dec 19, 2022
@davidpdrsn
Copy link
Member

I think it's worth exploring!

@dawid-nowak
Copy link
Author

Looking through the code i can see how this is can be "E-hard" :)

Perhaps another option would be to do something like this ?

let app = Router::new()
    .route(("/path", "application/json"), post(path_handler_for_json)
    .route("/path"), post(path_handler_for_default))

I think that would only require changes to Node and instead of str we woudl have a tuple (str, mime)

@jplatte
Copy link
Member

jplatte commented Dec 19, 2022

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)

@davidpdrsn
Copy link
Member

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

@dawid-nowak
Copy link
Author

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:

  1. Passing state around. In the original code it looks like state is defaulted to () and then a new instance of Router is returned if we want to use a different state here. It looks like this won't work with a trait so then it means that state has to be stored inside of whatever implements RouteResolver and unfortunately once Router is instantiated the type of state can't change.
  2. The RouteResolver interface. It is not the prettiest or intutive. I think this is because of very tight coupling between existing Router and MethodRouter and ability to nest Routers etc.

@davidpdrsn
Copy link
Member

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 content-type based routing for and think if we can come up with a simpler solution to that problem, instead of immediately assuming we need generalized "route by anything".

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

Feels like you're misunderstanding the example. The extractor doesn't have to be an enum. You could also do struct JsonOrForm<T>(T);. Then all the branching will be done in the extractor. Perhaps we should change the example to that to make it more clear.

@dawid-nowak
Copy link
Author

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:

import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/users")
public class UserControllerConsume {

    @RequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
    public String handleJson(@RequestBody String s) {
        System.out.println("json body : " + s);
        return "";
    }

    @RequestMapping(consumes = MediaType.APPLICATION_XML_VALUE)
    public String handleXML(@RequestBody String s) {
        System.out.println("xml body " + s);
        return "";
    }
}

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 :

fn path_handler_for_json(Json(payload):Json<Payload>){
}

fn path_handler_for_xml(Xml(payload):Xml<OtherPayload>){
}

fn path_handler_for_text(payload:String){
}

let app = Router::new()
    .route("/path", post(path_handler_for_json).consumes("application/json").produces("application/json"))
    .route("/path", post(path_handler_for_xml).consumes("application/xml").produces("application/xml"))
    .route("/path"), "post(path_handler_for_text).consumes("text/plain").produces("text/plain"))

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.

@davidpdrsn
Copy link
Member

I'm still not convinced that making the routing more complicated is worth it for this use case.

Using an extractor to handle different content-types still feels like the right approach to me. We could also investigate adding helpers to make it easier to write such extractors. Feels to me like that would be easier.

@dawid-nowak
Copy link
Author

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.

@davidpdrsn
Copy link
Member

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.

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.

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.

@Kinrany
Copy link
Contributor

Kinrany commented Jan 31, 2023

Perhaps for the purpose of keeping axum minimalistic, routing as a whole should be pulled out of axum.

@davidpdrsn
Copy link
Member

@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.

@dawid-nowak
Copy link
Author

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.
If (and this is a big if) the api needs to handle many different mime types, we could end up with something like this:

endpoint1_handler_get(request){
switch(request.content_type){
case application/json: endpoint1_sub_handler_get_json
case application/xml endpoint1_sub_handler_get_xml
case application/form endpoint1_sub_handler_get_xml
}
}

endpoint2_handler_get(request){
switch(request.content_type){
case application/json: endpoint2_sub_handler_get_json
case application/xml endpoint2_sub_handler_get_xml
case application/form endpoint2_sub_handler_get_xml
}
}

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.
In this case it would be more beneficial to check the content type and respond with 406 Not Acceptable before de-compressing the body. Ideally, the whole process would be in a layer/service so the handler doesn't have to be aware of the process.

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.

@davidpdrsn
Copy link
Member

I've been thinking that you can use Handler::or from axum-extra to get something pretty similar to the top comment:

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.

@dawid-nowak
Copy link
Author

dawid-nowak commented Mar 10, 2023

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)) 

@davidpdrsn
Copy link
Member

davidpdrsn commented Mar 10, 2023

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.

@dawid-nowak
Copy link
Author

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())

@davidpdrsn
Copy link
Member

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.

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

I don't see the problem. Compressing "nothing" is basically free.

Would something like this perhaps work ?

You could make such an api I think so yes. or is implemented in axum-extra and doesn't require special treatment from axum. It just implements the Handler trait. So you could imagine other ways to do that.

@dawid-nowak
Copy link
Author

I don't see the problem. Compressing "nothing" is basically free.

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 ?

@davidpdrsn
Copy link
Member

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.

@dawid-nowak
Copy link
Author

dawid-nowak commented Mar 11, 2023

Ahh, cool.
Little bit out of topic, but will this work ? We want to de-compress request , do something in a handler and then compress response body. Is the order of route_layer relevant here?

post(json).route_layer(RequestDecompressionLayer::new()).route_layer(CompressionLayer::new())

@davidpdrsn
Copy link
Member

Nope the order shouldn't matter.

You can also use a tuple of layers (that's a layer as well)

post(json).route_layer((RequestDecompressionLayer::new(), CompressionLayer::new()))

@Ducky2048
Copy link

@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.

@shepmaster
Copy link

shepmaster commented Jun 26, 2023

We could put it in axum-extra so users can try it out.

Yes, please! ❤️

... Actually, I'd want this for the Accept header, which I see has an interesting backstory. That may be a sign that the functionality may want to be a bit more composable, to allow for arbitrary headers.

@ttys3
Copy link
Contributor

ttys3 commented Aug 2, 2023

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?

@davidpdrsn
Copy link
Member

You can extract the headers with axum::http::HeaderMap and do whatever you need in the handler.

@shepmaster
Copy link

For my case, I implemented FromRequestParts and can then do different things in my handler. It’s definitely possible to do today.

@docteurklein
Copy link

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 Accept header rules (priorities and fallbacks like q=0.8), it maybe needs to know ahead of time of all the routes constraints, and not let the decision be made by just cascading failing handlers.

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 Accept-Encoding or Accept-Language.

@davidpdrsn
Copy link
Member

If you let handlers take care of this, will introspection still be possible to help generators like https://github.com/jakobhellermann/axum_openapi?

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.

@domenicquirl
Copy link

Starting from Content-Type, the discussion so far has been primarily about headers that affect the representation of a resource (either one that is sent to the server or one that is returned). It may be worth to explicitly differentiate between headers that may parameterize a resource (like Accept / Accept-Language / Content-Type) and headers that parameterize the entire Site / Router.

An example of the latter could be to allow versioning of an API through an "API Version" header: C# / .NET, for example, supports
inferring an API Version from any or all of URL Path, Query Parameters, a specific Header or the Media Type (either as a parameter or as part of a vnd. Vendor MIME Type) via its API Version Reader. Arguably, "API Version" has a different meaning depending on which of these you use - GET /api/v1/something probably versions the API itself, while GET /api/something with an Accept: vnd.foobar-inc.v1+json header could also version the data format used for "Something" independently.

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 X-GitHub-Api-Version header containing the API version's release date to specify the version to be accessed: GitHub Documentation.

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 /something endpoint happens inside the handler and the routing doesn't clearly indicate which routes / endpoints are available in what version(s).

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 nest instead of single route so you can build individual routers for each version, and you'd probably also want to be able to attach different middleware down the line for things like giving separate access scopes to different API endpoints within a version.

@commonsensesoftware
Copy link

Accept and Content-Type aren't routing decisions, but they definitely affect serialization and deserialization. Given how handlers are currently mapped, I can see how that has an appearance to route somewhere and to a potentially alternate implementation.

What I would want and expect from a router is automatic handling of invalid requests from configured metadata. For example, if Content-Type: application/json is sent by a client, but the handler is configured to only allow application/merge-patch+json, then I would expect the router to short-circuit the request with 415 (Unsupported Media Type). Similarly, if a client specifies Accept: application/xml and the route can only produce application/json, then the router should short-circuit with 406 (Not Acceptable). 406 is often a challenge because there is no definitive way to know for sure what that handler will actually return in a response until the Content-Type header is written. If the handler defines what will produce, that is a reasonable implementation approach. If the handler implementation goes of the rails from what was configured, then caveat emptor.

It's also worth pointing out that Accept and Content-Type do not have to be symmetrical. If they are specifically mapped by routing, that could get messy. For example:

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 Accept. By spec, if Accept isn't specified, then the server can assume the client will accept any media type (effectively */*). In the same fashion, a route handler that doesn't say which content it consumes, if it consumes content, either consumes anything supported by code or is imperatively handled in its own code.

In my experience, most service authors tend to prefer well-behaved HTTP semantics which are configured declaratively (e.g consumes and produces).

@commonsensesoftware
Copy link

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 axum. I'm still getting myself up to speed with how the basics work before I comment too heavily. I'm sure such a capability would be a separate crate, but I imagine it will certainly circle back into the routing discussion here at a some point.

Today, it seems that implementing versioning is very limited. Option one would be versioning within a URL segment (e.g. /v1/resource), which works, but is the worst way to do it and isn't RESTful (as it violates the Uniform Interface constraint despite being very common). All other methods seem to push down into the individual route handlers.

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 axum for high-level constructs because you can't register multiple handlers with the same route path. There should be a way to add the same route path if there is another way to disambiguate them - say by API version. However if path + metadata is a duplicate, then that should still be an error.

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.

@docteurklein
Copy link

docteurklein commented Jan 5, 2024

Accept and Content-Type aren't routing decisions

I already see limitations in axum for high-level constructs because you can't register multiple handlers with the same route path.

I find these 2 quotes interesting because it shows how arbitrary what is deemed valid as "routing".
I understand that what matters publicly is having:

  • a canonical URI to a resource
  • a representation, negotiated by Accept header
  • verbs to act on said resource

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 Accept header."

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.

@davidpdrsn
Copy link
Member

I still believe what I suggested here is worth exploring in a separate crate.

@Nutomic
Copy link

Nutomic commented Jan 19, 2024

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.

@jedenastka
Copy link

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.

@jason-johnson
Copy link

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. get function for a resource with a match statement for all the supported versions until something better comes along.

@jason-johnson

This comment was marked as off-topic.

@jplatte
Copy link
Member

jplatte commented Aug 25, 2024

@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).

@jason-johnson
Copy link

Done: #2888

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-axum-extra C-feature-request Category: A feature request, i.e: not implemented / a PR. E-hard Call for participation: Experience needed to fix: Hard / a lot
Projects
None yet
Development

No branches or pull requests