Actix-web middleware redirect with extension data, please help

Hi,

Please kindly help with the following problem.

Using actix-web, from a middleware, I'm redirecting to /login route with some extension message attached to the request, then in the /login route handler, I try to extract the message and just print it to the console -- but there is no message!

I am using actix examples middleware redirect as my learning example.

I make the following modifications to the above middleware:

...
//behai: added HttpMessage
use actix_web::{
    body::EitherBody,
    dev::{self, Service, ServiceRequest, ServiceResponse, Transform},
    http, Error, HttpResponse, HttpMessage, 
};
...

//behai: added struct Msg
#[derive(Debug, Clone)]
pub struct Msg(pub String);

...
    fn call(&self, request: ServiceRequest) -> Self::Future {
        // Change this to see the change in outcome in the browser.
        // Usually this boolean would be acquired from a password check or other auth verification.
        let is_logged_in = false;

        // Don't forward to `/login` if we are already on `/login`.
        if !is_logged_in && request.path() != "/login" {
            let (request, _pl) = request.into_parts();

            let response = HttpResponse::Found()
                .insert_header((http::header::LOCATION, "/login"))
                .finish()
                // constructed responses map to "right" body
                .map_into_right_body();
				
            // behai: Attached message to request.
            request.extensions_mut().insert::<Msg>(Msg("test message".to_owned()));
				
            return Box::pin(async { Ok(ServiceResponse::new(request, response)) });
        }

        let res = self.service.call(request);

        Box::pin(async move {
            // forwarded responses map to "left" body
            res.await.map(ServiceResponse::map_into_left_body)
        })
    }

Please note, in the code above, I attach a custom message via:

request.extensions_mut().insert::<Msg>(Msg("test message".to_owned()));

And following is the helper method which render the login page. I try to extract the custom message at the start, but there is always None:

fn render_login_page(msg: Option<ReqData<Msg>>) -> String {
    let message: String = match msg {
        None => String::from("No message found."),
        Some(msg_data) => msg_data.into_inner().0.clone(),
    };

    println!("render_login_page(): [{}]", message);

    // Create a new Tera instance and add a template from a string
    let tera = Tera::new("templates/auth/**/*").unwrap();

    // let mut ctx = Context::new();
    let ctx = Context::new();

    tera.render("login.html", &ctx).expect("Failed to render template")
}

And following is my /login handler which calls render_login_page:

#[get("/login")]
pub async fn login_page(
    msg: Option<ReqData<Msg>>
) -> impl Responder {

    HttpResponse::Ok()
        .content_type("text/html; charset=utf-8")
        .body(render_login_page(msg))
}

I've done a lot of reading on middleware, but so far, I've not been able to figure it out yet.

Prior to this, I've experimented with the SayHi middleware, and attach this same custom data to it, and I'm able to get the custom message out.

-- But the implementation of the call method between the two are quite different.

I'm sure there is something about the creation or the request and the response that I have no clue about?

Thank you and best regards,

...behai.

I've never seen Option<ReqData<T>>, my first guess is that extraction doesn't work if you nest the extractor in a non-extractor type like Option. Could try and see whether you get the message if you remove the Option around ReqData<Msg>?

Never mind, the example of ReqData acually shows ReqData being wrapped in Option.

1 Like

I think you put this linke in the wrong place in your middleware. You only insert it when you respond with a redirection to your login page, not when you actually call your login page handler. Your login page is called here:

so I think you get your message correctly, when you make the following change to your middleware:

    fn call(&self, request: ServiceRequest) -> Self::Future {
        // Change this to see the change in outcome in the browser.
        // Usually this boolean would be acquired from a password check or other auth verification.
        let is_logged_in = false;

        // Don't forward to `/login` if we are already on `/login`.
        if !is_logged_in && request.path() != "/login" {
            let (request, _pl) = request.into_parts();

            let response = HttpResponse::Found()
                .insert_header((http::header::LOCATION, "/login"))
                .finish()
                // constructed responses map to "right" body
                .map_into_right_body();
				
-           // behai: Attached message to request.
-           request.extensions_mut().insert::<Msg>(Msg("test message".to_owned()));

            return Box::pin(async { Ok(ServiceResponse::new(request, response)) });
        }

+       // behai: Attached message to request.
+       request.extensions_mut().insert::<Msg>(Msg("test message".to_owned()));

        let res = self.service.call(request);

        Box::pin(async move {
            // forwarded responses map to "left" body
            res.await.map(ServiceResponse::map_into_left_body)
        })
2 Likes

Good morning jofas,

Thank you very much your helps. You are right, I made the change as per your suggestion, and it works.

Clearly, I don't yet understand what:

return Box::pin(async { Ok(ServiceResponse::new(request, response)) });

I will need to do more exercises on this subject.

Thank you and best regards,

...behai.

What your middleware is doing is basically redirecting your user to /login in case they aren't logged in (is_logged_in == false) and not trying to access /login to begin with (which would cause an endless loop where your server responds to a request for /login with "yes, you can access /login, but first visit /login to authenticate yourself").

So

is pretty much working like a normal function with an early return where you respond to your client with a http response with status code 302, without ever entering the handler for the route the user was trying to access.

The workflow is basically like this:

  1. Your client asks your server to access /some_route
  2. Your server calls your login middleware
  3. Your middleware sees the user is not logged in and therefore is not allowed to call this route. We respond to the client with a redirection to /login (that's what is happening in the if statement of your middleware)
  4. Your client (probably a web browser like Chrome or Firefox) sees the redirect. It looks where it should redirect to (the location header of the http response)
  5. Your client makes a new http request to /login. This new request is completely independent of the first request we made. Your middleware will be called anew and this time it will not enter the if statement, because the user—even though they are still not signed in—is trying to access /login this time.
  6. Your middleware adds the Msg as an extension to the request
  7. Your middleware continues by passing the request to your /login handler (let res = self.service.call(request);
  8. Your /login handler extracts Msg, does something with it and returns some http response to the client

Hope that helps you in understanding what your middleware is doing there.

1 Like

Good evening jofas,

Thank you very much for the explanation of the workflow -- particularly point 5. I did not know that at all until your explanations. Now I understand what I have done wrong.

Best regards,

...behai.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.