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

Missing extensions are runtime errors rather than compile errors #1142

Closed
davidpdrsn opened this issue Jul 2, 2022 · 4 comments · Fixed by #1155
Closed

Missing extensions are runtime errors rather than compile errors #1142

davidpdrsn opened this issue Jul 2, 2022 · 4 comments · Fixed by #1155
Assignees
Labels
A-axum 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
Milestone

Comments

@davidpdrsn
Copy link
Member

Attempting to extract Extension<Foo> without having .layer(Extension(foo)) will result in a runtime error, rather than a compile error. That has been a long standing problem since 0.1. We don't have a good solution and ergonomic solution to it yet but I think its something we should experiment with for 0.6.

@davidpdrsn davidpdrsn 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 labels Jul 2, 2022
@davidpdrsn davidpdrsn added this to the 0.6 milestone Jul 2, 2022
@davidpdrsn
Copy link
Member Author

davidpdrsn commented Jul 4, 2022

I have been thinking about this and have a proposal now.

The reason its hard in the first place is that Router::route takes any T: Service. So even if we added some state generic to Router it wouldn't be able to verify that the routes uses that same state type, as it just sees a generic T: Service.

My proposal is to change Router so instead its API looks like this

#[tokio::main]
async fn main() {
    let mut app = Router::new(AppState {});

    // can also consider having `Router::with_state(...)` and
    // then `Router::new()` just being `with_state(())`

    // add a route for `GET /` that goes to `handler`
    app.get("/", handler);

    // add routes for both `GET /foo` and `POST /foo`
    app.get("/foo", handler).post(handler);

    // this wouldn't work
    // the thing returned by `get` doesn't allow adding routes with new paths
    // just new methods
    // this we can still debate but its not strictly relevant to this issue
    // app.get("/something", handler).post("/something-else", bar);

    axum::Server::bind(...)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

// the specific state our app needs
#[derive(Clone)]
struct AppState {}

async fn handler() {}

Now Router::{get, post, etc} can take the handler itself as the argument without any indirection.

Next add a new S (for state) type param to handler and accept the state in call:

pub trait Handler<S, T, B = Body>: Clone + Send + Sized + 'static {
    type Future: Future<Output = Response> + Send + 'static;

    fn call(self, state: S, req: Request<B>) -> Self::Future;
}

The signature of Router::get would be

impl<B> Router<B> {
    pub fn get<H, S, T>(mut self, path: &str, handler: T) -> Self
    where
        H: Handler<S, T, B>
    {
        // ...
    }
}

Next the same state is present in RequestParts

impl<S, B> RequestParts<S, B> {
    // only immutable access to the state
    pub fn state(&self) -> &S {
        &self.state
    }
}

This changes FromRequest to

#[async_trait]
pub trait FromRequest<S, B>: Sized {
    type Rejection: IntoResponse;

    async fn from_request(req: &mut RequestParts<S, B>) -> Result<Self, Self::Rejection>;
}

Next we add a State extractor that just accesses the state from RequestParts:

pub struct State<S>(pub S);

#[async_trait]
impl<S, B> FromRequest<S, B> for State<S>
where
    S: Clone
{
    type Rejection = Infallible;

    async fn from_request(req: &mut RequestParts<S, B>) -> Result<Self, Self::Rejection> {
        Ok(Self(req.state().clone()))
    }
}

I think that should work. Router::get binds the state param and its carried all the way through to the extractor. So if you extracted the wrong state type your handler wouldn't implement Handler<S, ...>. It would implement Handler for some other S2 type, which wouldn't compile.

nest, nest_service, and merge

nest, nest_service, and merge would work pretty much like they do today but I think should support "upcasting" the state so you can do app.nest("/api", api_router.map_state(Into::into)).

Routing to services

We should still provide Router::*_service methods for routing directly to Services:

let app = Router::new(());

app.get_service("/", some_service).post(handler).patch(service);

While this is a pretty big change to the API I think I'm fine with it. I don't love that we need .get(handler) and .get_service(service). Feels a bit redundant but don't think there is a way around that. Crucially its really only Router that changes. FromRequest and Handler change a bit but IntoResponse remains unchanged.

Thoughts?

@davidpdrsn
Copy link
Member Author

Could also consider doing

app.at("/foo").get(handler).post(handler);

It is perhaps more symmetric 🤷 I haven't thought much about this and have to play around with it. Note this is exactly what tide's router does.

@jplatte
Copy link
Member

jplatte commented Jul 4, 2022

This would be a lot of churn, removing MethodRouter completely. Could it possibly still exist with this API (but maybe not implement Service because it also needs to have that state passed when being invoked)?

@davidpdrsn
Copy link
Member Author

Hm yeah that could work if MethodRouter doesn't implement Service. I didn't consider that.

I was a little worried that rust wouldn't be able to infer the state type if you do .route("/", get_service(...)) since get_service works for any state but that isn't a problem it turns out. See https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=b8dc4cd60721fd4fd62dd1c36110f43c

Thats way nicer and should result in less churn.

Do you have an opinion about the changes to Handler, FromRequest, and friends?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-axum 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

Successfully merging a pull request may close this issue.

2 participants