⬆⬇ Taming Redirects and Reloads: An Axum Adventure on Fly.io 🔃


Deploying web applications often involves ensuring traffic flows through custom domains. This post details configuring redirects for an Axum-based application on Fly.io, tackling a peculiar Chromium reload issue, and navigating a deployment snag.

Part 1: The Great Redirect - Forcing the Custom Domain

The goal: redirect your-app-name.fly.dev to https://example.com, preserving paths and queries. Axum middleware is perfect for this.

// src/main.rs
use axum::{
    extract::Host, http::{Request, Uri}, middleware::Next,
    response::{IntoResponse, Response, Redirect},
};

async fn redirect_fly_to_custom_domain(
    Host(host): Host,
    uri: Uri,
    request: Request<axum::body::Body>,
    next: Next,
) -> Response {
    if host.contains("fly.dev") {
        let path = uri.path();
        let query = uri.query().map(|q| format!("?{}", q)).unwrap_or_default();
        let redirect_url = format!("https://example.com{}{}", path, query);
        tracing::info!("Redirecting from {} to {}", host, redirect_url);
        return Redirect::permanent(&redirect_url).into_response();
 }
    next.run(request).await
}

Key Learning 1: Middleware Order is King!

Ensure redirect middleware runs before others like authentication for efficiency.

// src/main.rs (Router setup)
// ...
    .layer(middleware::from_fn(redirect_fly_to_custom_domain)) // Redirect first!
    .layer(middleware::from_fn_with_state(app_state.clone(), auth_middleware)) // Then auth
// ...

Key Learning 2: Don't Interfere with Health Checks!

Fly.io health checks might use the *.fly.dev domain. Exclude health check paths (e.g., /health) from redirects.

// src/main.rs (redirect_fly_to_custom_domain function)
// ...
    let path = uri.path();
    if host.contains("fly.dev") && !path.starts_with("/health") { // Added health check exclusion
        // ... redirect logic ...
 }
// ...

Part 2: The Mysterious Case of the Chromium Reload & The Graceful Fix

A strange issue can arise: reloading pages in Chromium browsers causes hangs. Firefox might be fine. This hints at connection handling, possibly related to abrupt terminations during reloads combined with redirects.

The solution: Graceful Shutdown for the Axum server. This allows ongoing requests to complete before the server stops, preventing abrupt disconnections.

// src/main.rs

// Helper function to listen for a shutdown signal (Ctrl+C)
async fn shutdown_signal() {
    tokio::signal::ctrl_c().await.expect("Failed to install CTRL+C handler");
    tracing::info!("Shutdown signal received, starting graceful shutdown");
}

#[tokio::main]
async fn main() {
    // ... (existing setup) ...

    let app = /* ... router definition ... */
        .layer(middleware::from_fn(redirect_fly_to_custom_domain))
        // ... other layers ...
    
    // ... (addr setup) ...
    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
    
    axum::serve(
        listener,
        app.into_make_service_with_connect_info::<SocketAddr>()
 )
    .with_graceful_shutdown(shutdown_signal()) // Crucial addition!
    .await
    .unwrap();
}

This can resolve Chromium reload issues by ensuring cleaner connection handling during server restarts (like Fly.io deployments).

Conclusion

This exploration highlighted several key aspects of web server development:

  1.  Middleware Order: Impacts efficiency and correctness.
  2.  Operational Needs: Account for platform features like health checks.
  3.  Graceful Shutdown: Essential for server stability and a smooth client experience, especially for resolving issues like the Chromium reload problem.

These steps help ensure an application redirects reliably, handles reloads smoothly, and deploys cleanly.