Interesting issue with new async closures with very cryptic error message

Hi,
I hit an interesting issue with async closure, I have code like this (with rustc 1.85), cut to minimum:

pub fn required_rolex<T>(
    role: impl Into<Role>,
) -> impl AsyncFn(ApiClaim, Request, Next) -> Response
where
{
    let inner_fn = async move |claim: ApiClaim, req: Request, next: Next| {
        if !claim.has_role(&role.into()) {
            return StatusCode::FORBIDDEN.into_response();
        }
        StatusCode::OK.into_response()
    };
    inner_fn
}

which gives me this rather cryptic error:

error[E0277]: the trait bound `i32: ops::async_function::internal_implementation_detail::AsyncFnKindHelper<i8>` is not satisfied
  --> crates/mbs4-app/src/auth/token.rs:53:6
   |
53 | ) -> impl AsyncFn(ApiClaim, Request, Next) -> Response
   |      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `ops::async_function::internal_implementation_detail::AsyncFnKindHelper<i8>` is not implemented for `i32`

When changing to normal closure:

pub fn required_rolex<T>(role: impl Into<Role>) -> impl Fn(ApiClaim, Request, Next) -> Response
where
{
    let inner_fn = move |claim: ApiClaim, req: Request, next: Next| {
        if !claim.has_role(&role.into()) {
            return StatusCode::FORBIDDEN.into_response();
        }
        StatusCode::OK.into_response()
    };
    inner_fn
}

I also receive error, but this time much more clear:

error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnOnce`
  --> crates/mbs4-app/src/auth/token.rs:54:20
   |
51 | pub fn required_rolex<T>(role: impl Into<Role>) -> impl Fn(ApiClaim, Request, Next) -> Response
   |                                                    -------------------------------------------- the requirement to implement `Fn` derives from here
...
54 |     let inner_fn = move |claim: ApiClaim, req: Request, next: Next| {
   |                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this closure implements `FnOnce`, not `Fn`
55 |         if !claim.has_role(&role.into()) {
   |                             ---- closure is `FnOnce` because it moves the variable `role` out of its environment
...
60 |     inner_fn
   |     -------- return type was inferred to be `{closure@crates/mbs4-app/src/auth/token.rs:54:20: 54:68}` here

And yes this seems to be the cause - changing to AsyncFnOnce in first case solves the issue.

I hope I guess cause right.

Minimal repro:

pub fn repro(foo: impl Into<bool>) -> impl AsyncFn() {
    let inner_fn = async move || {
        let _ = foo.into();
    };
    inner_fn
}

I don't see anything on the issue tracker mentioning AsyncFnKindHelper. This is probably an unknown bug. So, I filed a new one: impl AsyncFn inference error leaks confusing internal_implementation_detail::AsyncFnKindHelper trait constraint requirement · Issue #137905 · rust-lang/rust

Replacing the let inner_fn binding with directly returning the async closure gives a different error:

pub fn repro(foo: impl Into<bool>) -> impl AsyncFn() {
    async move || {
        let _ = foo.into();
    }
}
error[E0507]: cannot move out of `foo` which is behind a shared reference
 --> src/lib.rs:2:5
  |
2 |     async move || {
  |     ^^^^^^^^^^^^^ `foo` is moved here
3 |         let _ = foo.into();
  |                 ---
  |                 |
  |                 variable moved due to use in coroutine
  |                 move occurs because `foo` has type `impl Into<bool>`, which does not implement the `Copy` trait
  |
help: if `impl Into<bool>` implemented `Clone`, you could clone the value
 --> src/lib.rs:1:20
  |
1 | pub fn repro(foo: impl Into<bool>) -> impl AsyncFn() {
  |                   ^^^^^^^^^^^^^^^ consider constraining this type parameter with `Clone`
2 |     async move || {
3 |         let _ = foo.into();
  |                 --- you could clone this value

I wasn't aware of this limitation, but it suggests that the workaround is hoisting the .into() call out of the closure. And that indeed seems to fix it. At least in this minimal case:

pub fn repro(foo: impl Into<bool>) -> impl AsyncFn() {
    // Call `into()` before moving to the async closure.
    let foo = foo.into();

    async move || {
        let _ = &foo;
    }
}

Expanding back to the OP code, the hoisting workaround looks like this:

pub fn required_rolex(
    role: impl Into<Role>,
) -> impl AsyncFn(ApiClaim, Request, Next) -> Response {
    // Call `into()` before moving to the async closure.
    let role = role.into();

    let inner_fn = async move |claim: ApiClaim, req: Request, next: Next| {
        if !claim.has_role(&role) {
            return StatusCode::FORBIDDEN.into_response();
        }
        StatusCode::OK.into_response()
    };
    inner_fn
}

Aside: You have some pretty weird syntax, which is probably an error that was introduced as part of the minimization process. The trailing empty where clause and unused T parameter were a bit confusing.

@parasyte thanks for analyzing and logging issue - and yes the syntax issues are left overs as I tried to cut down original function to get to root of the problem, I then did not clean it up, as my head was confused by the error message :slight_smile:

The solution to problem is same I came into, I think I did meet similar problems in past - when one cannot move parameter to closure, solution was to create intermediate variable in function body scope - here with into, or clone.