Actix-web: how to determine if a called route was a redirect?

Hi,

Please help with the following question.

I have the below actix-web route handler method:

#[get("/some/route")]
pub async fn some_route_page(
    request: HttpRequest
) -> impl Responder {
    println!("some_route_page() [---\n{:#?}\n---]", request.headers());    

    HttpResponse::Ok()
        .content_type(ContentType::html())
        .body("using tera crate to load HTML from disk.")
}

This route can be called directly by a user with http://localhost:5000/some/route, or being programmatically redirected to by the server.

The headers in both use cases are indistinguishable.

How do I work out if it was a server redirect, please?

Thank you and best regards,

...behai.

This is a problem. The convention for redirections is to include a header (X-Forwarded-For) precisely to be able to differentiate requests like you want.

1 Like

That's new to me. I thought XFF and the standardized Forwarded header fields are for proxying, not redirection.

If both routes share the same domain, you could set a cookie in your redirection response which should be added to the redirected request.

1 Like

It's true that these are mostly used for proxying use cases. What I was trying to convey is that having the exact same headers between requests is a problem, and a header should be used to differentiate between the two cases.

And in that situation, it's preferable to use an existing header with semantic meaning.

2 Likes

That header is updated when forwarding via a proxy, not when redirecting. It's irrelevant for 3xx redirects.

3 Likes

You can't do that reliably. In some cases there may be Referer header sent by the browser when user is coming via a link, but that is often lost or intentionally removed by privacy features.

When your server redirects, you will need to change the URL, e.g. add ?redirected query string and look for that. If you set canonical <meta> or Link to the URL without the query string, search engines won't mind the extra query string.

1 Like

Hi @moy2010, @jofas and @kornel,

Thank you for your helps, discussions and suggestions.

-- I appreciate it. This makes my learning journey less scary :slight_smile:

Hi @jofas,

Thank you for the suggestion. I have tested this, and it appears to work for what I would like to do.

Thank you and best regards,

...behai.


For completeness, I have followed up @jofas suggestion:

And this is what I've working:

Refactored version of some_route_page(...) which consumes the cookie if set:

#[get("/some/route")]
pub async fn some_route_page(
    request: HttpRequest
) -> impl Responder {
    match request.cookie("redirect-message") {
        None => {},
        Some(c) => println!("some_route_page() redirect-message [{}]", c.value())
    }

    let mut cookie = build_redirect_cookie(request, "redirect-message", "");
    cookie.make_removal();    

    HttpResponse::Ok()
        .content_type(ContentType::html())
        .cookie(cookie)
        .body("using tera crate to load HTML from disk.")
		
}

New helper function build_redirect_cookie(...):

fn build_redirect_cookie<'a>(
    request: HttpRequest,
    name: &'a str,
    value: &'a str
) -> Cookie<'a> {
    // Header "host" should always be in the request headers.
    let host = request.headers().get("host").unwrap().to_str().unwrap().to_owned();
    // Remove the port if any.
    let parts = host.split(":");

    Cookie::build(name, value)
        .domain(String::from(parts.collect::<Vec<&str>>()[0]))
        .path("/")
        .secure(true)
        .http_only(true)
        .finish()    
}

Extracting the domain out of host header looks scary for me. I'll refactor it into a helper method later.

Server redirect code:

    ...
    let cookie= build_redirect_cookie(request, "redirect-message", "This page was redirected...");

    HttpResponse::Ok()
        .status(StatusCode::SEE_OTHER)
        .append_header((header::LOCATION, "/some/route"))
        .cookie(cookie)
        .finish()

StatusCode::SEE_OTHER should be used in this case, because the original user request is a POST, and the server redirects to a GET route.

Thank you again and best regards,

...behai.

1 Like

Can you maybe elaborate on why you want to detect it? What's the goal you're trying to accomplish? It's pretty unusual HTTP for it to matter whether something came redirected or not...

1 Like

Hi @scottmcm,

I do see the point of your question. I am not absolutely certain yet if this is the final approach yet.

The first POST request will be either a success or a "failure". In case of a "failure", I would like to redirect to /some/route with some specific notification, i.e., a message resulted from the "failure" case.

So far, I have not been able to persisted this message across to the route who does the rendering of the page for /some/route.

(In Python, the Flask web framework provided for redirect with some custom data, so it is quite easy to do.)

So I am settling for knowing that it was a redirect so that I can just display something generic to inform the users of the outcome.

This is not a production project or anything, I am just learning Rust at the moment.

Thank you and best regards,

...behai.

My default answer for that would be to redirect instead of /some/route?error=bad-email or something. That should be about as easy to consume as a header or cookie.

You could also consume that query key with Javascript, if you wanted, which could be written to update the URL to hide the query parameter if you wanted -- Javascript can do that without needing to actually navigate.

1 Like

Hi @scottmcm,

Thank you for your suggestion. I am aware of URL rewrite to hide query strings. But I am not too keen on that. For me, it is harder than Rust :slight_smile:

Thank you and best regards,

...behai.

Hi,

I thought an update is appropriate.

When I logged in using the application managed HTML pages, everything works fine.

But when I use JQuery AJAX POST calls, and request redirection occurs. The second request fails to read the cookies.

The cookies are session, http_only=true, SameSite::strict, secure=false (since I'm using it with only HTTP).

My CORS is as follows:

        let cors = Cors::default()
            .allowed_origin(&config.allowed_origin)
            .allowed_origin_fn(|origin, _req_head| {
                // Windows only for AJAX.
                origin.as_bytes().ends_with(b"localhost")
            })
            .allowed_methods(vec!["GET", "POST"])
            .allowed_headers(vec![
                header::CONTENT_TYPE,
                header::AUTHORIZATION,
                header::ACCEPT,
            ])
            .max_age(config.max_age)
            .supports_credentials();

Based on this Stack Overflow post How to persist data across routes using actix_session and RedisActorSessionStore, I can verify that adding these to my jQuery AJAX POST call solves the problem:

...
			xhrFields: {
			   withCredentials: true
			},
			crossDomain: true,
...

Best regards,

...behai.

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.