Actix-web / actix-web-lab, compiler gives error when in else block, please help

Hi,

I run another issue using actix-web and and actix_web_lab.

I am writing an actix-web-lab async function as a middleware. Depending on some condition, this middleware either forwards the existing response to the client as is, or creating a specific response and forwards this response to the client.

I am following this official example actix-web-lab/actix-web-lab/examples/from_fn.rs, particularly the functions async fn mutate_body_type and async fn mutate_body_type_with_extractors.

Both of "my" code blocks work fine individually, but when combine into if-else block, the compiler gives a scary looking error, while I understand the nature of the error, I don't know how to solve it. Please help.

To start off, this is the async function as a middleware, which has only a single code block, where I create ONLY a specific response and sends it to the client:

async fn finalise_request(
    req: ServiceRequest,
    next: Next<impl MessageBody + 'static>,
) -> Result<ServiceResponse<impl MessageBody>, Error> {
    let res = next.call(req).await?;

    Ok(res.map_body(move |_head, _body| 
        HttpResponse::Ok().body("behai<br/>Got stuck 🙀").into_body()
    ))
}

Where:

  • res is ServiceResponse<impl MessageBody>

  • _head is &mut RespondHead

  • _body is impl MessageBody

And it is registered with the application as follows:

pub async fn run(listener: TcpListener) -> Result<Server, std::io::Error> {
    ...

    let server = HttpServer::new(move || {
        App::new()
            ...
/*>>>*/     .wrap(from_fn(finalise_request))
            .wrap(auth_middleware::CheckLogin)
            ...
            .wrap(cors_config(&config))
            .service(
                ...
    })
    .listen_openssl(listener, ssl_builder())?
    .run();

    Ok(server)
}

This code compiles and works as expected, that is, if I access the login page, I would get this instead:

behai
Got stuck 🙀

And this version of finalise_request also works, it just forward the response as is to the clients, without it, the application does the same thing:

async fn finalise_request(
    req: ServiceRequest,
    next: Next<impl MessageBody + 'static>,
) -> Result<ServiceResponse<impl MessageBody>, Error> {
    let res = next.call(req).await?;

    Ok(res.map_into_left_body::<()>())
}

But this version of finalise_request, which I should need, gives scary compiler errors, which I am stuck:

async fn finalise_request(
    req: ServiceRequest,
    next: Next<impl MessageBody + 'static>,
) -> Result<ServiceResponse<impl MessageBody>, Error> {
    let res = next.call(req).await?;

    let xx: bool = false;

    if !xx {
        Ok(res.map_into_left_body::<()>())
    }

    else {
        Ok(res.map_body(move |_head, _body| 
            HttpResponse::Ok().body("behai<br/>got stuck :(").into_body()
        ))
    }
}

The error is in the else block, while I understand what error E0308 means, I don't know how to solve it:

error[E0308]: mismatched types
   --> src/lib.rs:223:12
    |
223 |           Ok(res.map_body(move |_head, _body| 
    |  _________--_^
    | |         |
    | |         arguments to this enum variant are incorrect
224 | |             HttpResponse::Ok().body("behai<br/>got stuck :(").into_body()
225 | |         ))
    | |_________^ expected `ServiceResponse<EitherBody<..., ...>>`, found `ServiceResponse`
    |
    = note: expected struct `ServiceResponse<EitherBody<impl MessageBody + 'static, ()>>`
               found struct `ServiceResponse<BoxBody>`
help: the type constructed contains `ServiceResponse` due to the type of the argument passed
   --> src/lib.rs:223:9
    |
223 |            Ok(res.map_body(move |_head, _body| 
    |  __________^__-
    | | _________|
    | ||
224 | ||             HttpResponse::Ok().body("behai<br/>got stuck :(").into_body()
225 | ||         ))
    | ||_________-^
    | |__________|
    |            this argument influences the type of `Ok`
note: tuple variant defined here
   --> C:\Users\behai\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib/rustlib/src/rust\library\core\src\result.rs:506:5
    |
506 |     Ok(#[stable(feature = "rust1", since = "1.0.0")] T),
    |     ^^

Thank you and best regards,

...behai.

Could you try:

    if !xx {
        Ok(res.map_into_left_body())
    } else {
        Ok(res.map_body(move |_head, _body| {
            HttpResponse::Ok().body("behai<br/>got stuck :(").map_into_right_body()
        }))
    }

please?

1 Like

Hi @jofas,

Thank you for your help, yet again :slight_smile:

I tried that, and it is still gives E0308:

error[E0308]: mismatched types
   --> src/lib.rs:221:12
    |
221 |           Ok(res.map_body(move |_head, _body| {
    |  _________--_^
    | |         |
    | |         arguments to this enum variant are incorrect
222 | |             HttpResponse::Ok().body("behai<br/>got stuck :(").map_into_right_body()
223 | |         }))
    | |__________^ expected `ServiceResponse<EitherBody<..., ...>>`, found `ServiceResponse<HttpResponse<...>>`
    |
    = note: expected struct `ServiceResponse<EitherBody<impl MessageBody + 'static, _>>`
               found struct `ServiceResponse<HttpResponse<EitherBody<_, BoxBody>>>`
help: the type constructed contains `ServiceResponse<HttpResponse<EitherBody<_>>>` due to the type of the argument passed
   --> src/lib.rs:221:9
    |
221 |            Ok(res.map_body(move |_head, _body| {
    |  __________^__-
    | | _________|
    | ||
222 | ||             HttpResponse::Ok().body("behai<br/>got stuck :(").map_into_right_body()
223 | ||         }))
    | ||__________-^
    | |___________|
    |             this argument influences the type of `Ok`
note: tuple variant defined here
   --> C:\Users\behai\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib/rustlib/src/rust\library\core\src\result.rs:506:5
    |
506 |     Ok(#[stable(feature = "rust1", since = "1.0.0")] T),
    |     ^^

Oops, forgot to call into_body on the HttpResponse you construct inside the ServiceResponse::map_body call:

    if !xx {
        Ok(res.map_into_left_body())
    } else {
        Ok(res.map_body(move |_head, _body| {
            HttpResponse::Ok().body("behai<br/>got stuck :(").map_into_right_body().into_body()
        }))
    }
2 Likes

Hi @jofas,

Thank you for your help. The compiler is happy now :slight_smile:

I will do more readings to better understand why this works.

I am still pretty much in the dark about ServiceResponse<impl MessageBody>, ServiceResponse<B>, etc., I understand it is about traits and implementation.

But I am not able yet to articulate the code to myself and others. I have now something which works in the direction that I would like to implement, this will aides with the reading of the documentations.

Thank you and best regards,

...behai.

Constructing middleware in actix-web is difficult. Or rather technically more advanced compared to building regular endpoints. A very important struct to know when creating middleware is EitherBody, which we used in your example. Often times you want to return early from a middleware. Like "hey, the user is not authorized, let's return a 401 here and stop processing this request any further." Something like:

if (unauthorized) {
  return Ok(HttpResponse::Unauthorized().finish());
}

service.call(req)

But this is a problem, because service.call(req)'s generic body type B doesn't have to be a BoxBody. That's a type mismatch, trying to return both ServiceResponse<B = BoxBody> (from HttpResponse::Unauthorized().finish()) and ServiceResponse<B = B> (from service.call(req)). We need to resolve this type mismatch by creating one common type from both BoxBody and B. That common type is EitherBody[1]:

use futures_util::future::FutureExt; // for the `map` method on `service.call(req)`

if (unauthorized) {
  return Ok(HttpResponse::Unauthorized().finish()).map_into_left_body();
}

service.call(req).map(|r| r.map_into_right_body())

  1. It could be another enum, of course, like either::Either, but EitherBody is well integrated into actix-web through methods like ServiceResponse::map_into_right_body ↩ī¸Ž

2 Likes

Thank you very much @jofas, I appreciate your explanations.

Hi,

I would just like to post an update for the shake of completeness.

The "final" version of the middleware before I carried out integration tests is:

/// Standalone, async middleware function.
/// 
/// References:
///     * [wrap_fn &AppRouting should use Arc<AppRouting> #2681](https://github.com/actix/actix-web/issues/2681)
///     * [Crate actix_web_lab](https://docs.rs/actix-web-lab/latest/actix_web_lab/index.html)
///     * [actix-web-lab/actix-web-lab/examples/from_fn.rs](https://github.com/robjtede/actix-web-lab/blob/7f5ce616f063b0735fb423a441de7da872847c5c/actix-web-lab/examples/from_fn.rs)
/// 
/// See also:
///     * [``users.rust-lang.org`` -- Actix-web / actix-web-lab, compiler gives error when in else block, please help](https://users.rust-lang.org/t/actix-web-actix-web-lab-compiler-gives-error-when-in-else-block-please-help/108925)
/// 
/// This method is responsible for the followings:
/// 
/// 1. If the request has been successfully completed, it looks for the updated access 
/// token String attachment in the request extension, if there is one, extracts it and 
/// forward it to the client via both the ``authorization`` header and cookie with the
/// returned response.
/// 
/// 2. If the request has not been successfully completed, translates the error struct 
/// attached in the request extension into a JSON serialisation response and forwards 
/// this new (mutated) response to the client.
/// 
async fn finalise_request(
    req: ServiceRequest,
    next: Next<impl MessageBody + 'static>,
) -> Result<ServiceResponse<impl MessageBody>, Error> {
    let mut updated_access_token: Option<String> = None;
    // Get set in src/auth_middleware.rs's 
    // fn update_and_set_updated_token(request: &ServiceRequest, token_status: TokenStatus).
    if let Some(token) = req.extensions_mut().get::<String>() {
        updated_access_token = Some(token.to_string());
    }

    let mut resp_error_status: Option<auth_middleware::ResponseErrorStatus> = None;
    if let Some(resp_err_stt) = req.extensions_mut().get::<auth_middleware::ResponseErrorStatus>() {
        resp_error_status = Some(resp_err_stt.clone());
    }

    let session_id = extract_session_id(&req);

    let mut res = next.call(req).await?;

    // No error comming out of auth_middleware.
    if resp_error_status.is_none() {
        if updated_access_token.is_some() {
            let token = updated_access_token.unwrap();

            res.headers_mut().append(
                header::AUTHORIZATION, 
                header::HeaderValue::from_str(token.as_str()).expect(TOKEN_STR_JWT_MSG)            
            );
    
            let _ = res.response_mut().add_cookie(
                &build_authorization_cookie(&token));
    
            tracing::debug!("Requested succeeded. Returning updated access token.");    
        }

        tracing::info!("Request {} exit", session_id);

        Ok(res.map_into_left_body())
    }
    else {
        let err_status = resp_error_status.unwrap();

        // println!(">>>>>\n\nerror_status {:#?}\n<<<<", err_status);

        tracing::debug!("Requested failed. Returning error status.");

        tracing::info!("Request {} exit", session_id);

        Ok(res.map_body(move |_head, _body| {
            HttpResponse::Unauthorized()
                // .status(err_status.code)
                .insert_header((header::CONTENT_TYPE, header::ContentType::json()))
                .cookie(remove_login_redirect_cookie())
                .cookie(remove_original_content_type_cookie())
                .body(serde_json::to_string(&err_status.body).unwrap())
                .map_into_right_body()
                .into_body()                
        }))
    }
}

fn extract_session_id(req: &ServiceRequest) -> String just returns a String::from("xxx").

In the else block, the value of error_status is:

ResponseErrorStatus {
    code: 401,
    body: ApiStatus {
        code: 401,
        message: Some(
            "Token is in error.",
        ),
        session_id: None,
    },
}

Some of my integration tests failed on checking the actual HTTP response status code.

Even though the response is HttpResponse::Unauthorized() but in the final response status code received at the client side is 200 (OK).

-- The JSON response body is correct, where the code field is 401 as set.

I can't explain why, but I think next.call(req).await?; has something to do with the response?

In the same official example actix-web-lab/actix-web-lab/examples/from_fn.rs, there is impl MyMw, and function async fn mw_cb, which I did not see before @jofas explanations, I switched the condition around based on this example:

async fn finalise_request(
    req: ServiceRequest,
    next: Next<impl MessageBody + 'static>,
) -> Result<ServiceResponse<impl MessageBody>, Error> {
    let mut updated_access_token: Option<String> = None;
    // Get set in src/auth_middleware.rs's 
    // fn update_and_set_updated_token(request: &ServiceRequest, token_status: TokenStatus).
    if let Some(token) = req.extensions_mut().get::<String>() {
        updated_access_token = Some(token.to_string());
    }

    let mut resp_error_status: Option<auth_middleware::ResponseErrorStatus> = None;
    if let Some(resp_err_stt) = req.extensions_mut().get::<auth_middleware::ResponseErrorStatus>() {
        resp_error_status = Some(resp_err_stt.clone());
    }

    let session_id = extract_session_id(&req);

    // No error comming out of auth_middleware.
    if resp_error_status.is_some() {
        let err_status = resp_error_status.unwrap();

        tracing::debug!("Requested failed. Returning error status.");

        tracing::info!("Request {} exit", session_id);

        Ok(req.into_response(//HttpResponse::Unauthorized()
            HttpResponse::Ok()
                .status(err_status.code)
                .insert_header((header::CONTENT_TYPE, header::ContentType::json()))
                .cookie(remove_login_redirect_cookie())
                .cookie(remove_original_content_type_cookie())
                .body(serde_json::to_string(&err_status.body).unwrap())        
        ).map_into_right_body())
    }    
    else {
        let mut res = next.call(req).await?;

        if updated_access_token.is_some() {
            let token = updated_access_token.unwrap();

            res.headers_mut().append(
                header::AUTHORIZATION, 
                header::HeaderValue::from_str(token.as_str()).expect(TOKEN_STR_JWT_MSG)            
            );
    
            let _ = res.response_mut().add_cookie(
                &build_authorization_cookie(&token));
    
            tracing::debug!("Requested succeeded. Returning updated access token.");    
        }

        tracing::info!("Request {} exit", session_id);

        Ok(res.map_into_left_body())
    }
}

And I get the expected HTTP response status code at the client side.

I think Ok(req.into_response(...).map_into_right_body()) prepares the response before sending it, so the response is intact?

Thank you and best regards,

...behai.

1 Like