How does axum::routing::Route::fallback differ from fallback_service for handling unmatched routes?

fallback accepts a handler function (like Router::route handlers), while fallback_service accepts any Service type for maximum flexibility. Both serve the same purposeβ€”handling requests that don't match any defined routesβ€”but differ in what they accept and how they're used. fallback provides a simpler API for common cases, while fallback_service enables advanced scenarios like delegating to another router or integrating non-handler services.

Basic Fallback Behavior

use axum::{Router, routing::get, response::IntoResponse, http::StatusCode};
 
async fn basic_fallback() {
    // Without any fallback, unmatched routes return 404 Not Found
    let app = Router::new()
        .route("/hello", get(|| async { "Hello!" }));
    
    // Requests to /hello -> "Hello!"
    // Requests to /unknown -> 404 Not Found (default)
    
    // With fallback, unmatched routes go to your handler
    let app = Router::new()
        .route("/hello", get(|| async { "Hello!" }))
        .fallback(fallback_handler);
    
    // Requests to /unknown -> fallback_handler handles it
}
 
async fn fallback_handler() -> impl IntoResponse {
    (StatusCode::NOT_FOUND, "Custom 404 - Page not found")
}

A fallback catches any request that doesn't match defined routes, letting you customize the 404 response.

Using fallback with Handler Functions

use axum::{Router, routing::get, response::IntoResponse, http::{StatusCode, Uri, Method}, extract::State};
 
async fn fallback_usage() {
    let app = Router::new()
        .route("/", get(root))
        .route("/users", get(list_users))
        .route("/users/{id}", get(get_user))
        .fallback(not_found);
    
    // fallback accepts async functions, same as route handlers
}
 
async fn root() -> &'static str {
    "Welcome to the API"
}
 
async fn list_users() -> &'static str {
    "User list"
}
 
async fn get_user() -> &'static str {
    "User details"
}
 
// Fallback handler receives the request URI and method
async fn not_found(uri: Uri, method: Method) -> impl IntoResponse {
    (StatusCode::NOT_FOUND, format!("No route for {} {}", method, uri))
}

fallback integrates with extractors just like regular route handlers.

Fallback with State Access

use axum::{Router, routing::get, extract::State, response::IntoResponse, http::StatusCode};
use std::sync::Arc;
 
struct AppState {
    api_version: String,
}
 
async fn fallback_with_state() {
    let state = Arc::new(AppState {
        api_version: "v1.0".to_string(),
    });
    
    // fallback handlers can access state via extractors
    let app = Router::new()
        .route("/api/data", get(get_data))
        .fallback(handle_not_found)
        .with_state(state);
}
 
async fn get_data() -> &'static str {
    "Data"
}
 
async fn handle_not_found(
    State(state): State<Arc<AppState>>,
) -> impl IntoResponse {
    (
        StatusCode::NOT_FOUND,
        format!("API {} - Unknown endpoint", state.api_version)
    )
}

Fallback handlers can use any extractor that normal handlers use, including State.

Using fallback_service with Services

use axum::{Router, routing::get, response::IntoResponse, http::StatusCode};
use tower::ServiceBuilder;
use tower_http::services::ServeDir;
 
async fn fallback_service_basic() {
    // fallback_service accepts any Service<Request>
    
    // Example 1: Serve static files as fallback (SPA routing)
    let app = Router::new()
        .route("/api/data", get(|| async { "Data" }))
        .fallback_service(ServeDir::new("dist"));
    
    // Requests to /api/data -> handled by route
    // Other requests -> served from "dist" directory
    // This enables SPA routing where the frontend handles all routes
}
 
async fn fallback_service_custom() {
    // Example 2: Custom service for fallback
    use tower::Service;
    use std::task::{Context, Poll};
    use http::{Request, Response};
    use futures::future::ready;
    
    // Custom service that always returns 404
    struct NotFoundService;
    
    impl<B> Service<Request<B>> for NotFoundService {
        type Response = Response<String>;
        type Error = std::convert::Infallible;
        type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
        
        fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
            Poll::Ready(Ok(()))
        }
        
        fn call(&mut self, request: Request<B>) -> Self::Future {
            ready(Response::builder()
                .status(StatusCode::NOT_FOUND)
                .body(format!("Not found: {}", request.uri()))
                .unwrap())
        }
    }
    
    let app = Router::new()
        .route("/api/data", get(|| async { "Data" }))
        .fallback_service(NotFoundService);
}

fallback_service accepts any type implementing Service<Request>, enabling integration with tower services.

Chaining Routers with fallback_service

use axum::{Router, routing::get, response::IntoResponse, http::StatusCode};
 
async fn router_delegation() {
    // Primary API router
    let api_router = Router::new()
        .route("/users", get(|| async { "Users" }))
        .route("/posts", get(|| async { "Posts" }));
    
    // Legacy API router  
    let legacy_router = Router::new()
        .route("/user/list", get(|| async { "Legacy users" }))
        .route("/post/list", get(|| async { "Legacy posts" }));
    
    // Use fallback_service to delegate unmatched routes to legacy router
    let app = Router::new()
        .route("/", get(|| async { "Home" }))
        .nest("/api", api_router)
        .fallback_service(legacy_router);
    
    // / -> "Home"
    // /api/users -> "Users"
    // /user/list -> "Legacy users" (handled by fallback_service)
    // /post/list -> "Legacy posts"
}

fallback_service enables delegating unmatched requests to another router.

SPA Routing Pattern

use axum::{Router, routing::{get, post}, response::IntoResponse, http::StatusCode};
use tower_http::services::{ServeDir, ServeFile};
 
async fn spa_routing() {
    // Single Page Application routing pattern
    
    // API routes are explicitly defined
    let api_routes = Router::new()
        .route("/api/users", get(get_users))
        .route("/api/posts", get(get_posts))
        .route("/api/login", post(login));
    
    // All other routes serve the SPA
    let app = Router::new()
        .merge(api_routes)
        .fallback_service(ServeFile::new("dist/index.html"));
    
    // /api/users -> get_users handler
    // /about -> serves index.html (SPA handles routing client-side)
    // /dashboard -> serves index.html
    // /any/path -> serves index.html
}
 
async fn get_users() -> &'static str { "Users" }
async fn get_posts() -> &'static str { "Posts" }
async fn login() -> &'static str { "Login" }

This is a common pattern: fallback_service serves the SPA entry point for all non-API routes.

Key Differences Between fallback and fallback_service

use axum::{Router, routing::get, response::IntoResponse, http::StatusCode};
use tower::ServiceBuilder;
 
async fn differences_summary() {
    // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    // β”‚ Aspect               β”‚ fallback              β”‚ fallback_service    β”‚
    // β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    // β”‚ Accepts              β”‚ Handler function      β”‚ Any Service         β”‚
    // β”‚ Type constraint      β”‚ Handler trait         β”‚ Service trait      β”‚
    // β”‚ Extractor support    β”‚ Yes (via extractors)  β”‚ Manual handling     β”‚
    // β”‚ State access         β”‚ Via State extractor   β”‚ Depends on service  β”‚
    // β”‚ Simplicity           β”‚ Simpler for handlers  β”‚ More flexible       β”‚
    // β”‚ Use case             β”‚ Custom 404 handlers   β”‚ Serve files, router β”‚
    // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    
    // fallback: simpler, for handler-based responses
    let app1 = Router::new()
        .route("/api", get(|| async { "API" }))
        .fallback(|uri| async move {
            (StatusCode::NOT_FOUND, format!("No route: {}", uri))
        });
    
    // fallback_service: more flexible, for services
    let app2 = Router::new()
        .route("/api", get(|| async { "API" }))
        .fallback_service(ServeDir::new("public"));
}

The choice depends on what you need: handlers vs. arbitrary services.

Error Handling in Fallbacks

use axum::{Router, routing::get, response::{IntoResponse, Response}, http::StatusCode};
use axum::extract::{Path, Query};
use std::collections::HashMap;
 
async fn fallback_error_handling() {
    // fallback handler can return errors like regular handlers
    let app = Router::new()
        .route("/api/health", get(|| async { "OK" }))
        .fallback(fallback_with_validation);
}
 
async fn fallback_with_validation(
    uri: axum::http::Uri,
    Query(params): Query<HashMap<String, String>>,
) -> Result<impl IntoResponse, AppError> {
    // You can use Result just like regular handlers
    if uri.path().contains("..") {
        return Err(AppError::PathTraversal);
    }
    
    // Dynamic routing logic in fallback
    if let Some(id) = params.get("id") {
        return Ok(format!("Fallback for ID: {}", id));
    }
    
    Ok((StatusCode::NOT_FOUND, "Unknown path".to_string()))
}
 
enum AppError {
    PathTraversal,
}
 
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        match self {
            AppError::PathTraversal => {
                (StatusCode::BAD_REQUEST, "Invalid path").into_response()
            }
        }
    }
}

Fallback handlers can use error handling patterns identical to regular handlers.

Middleware on Fallbacks

use axum::{Router, routing::get, response::IntoResponse, http::StatusCode};
use tower::ServiceBuilder;
use tower_http::{trace::TraceLayer, cors::CorsLayer};
 
async fn fallback_middleware() {
    // Router-level middleware applies to fallback too
    let app = Router::new()
        .route("/api/data", get(|| async { "Data" }))
        .fallback(|| async { (StatusCode::NOT_FOUND, "Not found") })
        .layer(TraceLayer::new_for_http())  // Applies to fallback
        .layer(CorsLayer::permissive());    // Applies to fallback
    
    // But you can also layer just the fallback_service
    let app2 = Router::new()
        .route("/api/data", get(|| async { "Data" }))
        .fallback_service(
            ServiceBuilder::new()
                .layer(TraceLayer::new_for_http())
                .service(ServeDir::new("public"))
        );
}

Router middleware applies to the fallback; fallback_service can have its own middleware stack.

Practical Example: Multi-tenant Routing

use axum::{Router, routing::get, extract::{State, Host}, response::IntoResponse, http::StatusCode};
use std::sync::Arc;
use std::collections::HashMap;
 
struct AppState {
    tenants: HashMap<String, Router<Arc<AppState>>>,
}
 
async fn multi_tenant_example() {
    let tenant_a = Router::new()
        .route("/dashboard", get(|| async { "Tenant A Dashboard" }));
    
    let tenant_b = Router::new()
        .route("/dashboard", get(|| async { "Tenant B Dashboard" }));
    
    let mut tenants = HashMap::new();
    tenants.insert("tenant-a.example.com".to_string(), tenant_a);
    tenants.insert("tenant-b.example.com".to_string(), tenant_b);
    
    let state = Arc::new(AppState { tenants });
    
    // Main router delegates to tenant routers
    let app = Router::new()
        .route("/", get(|| async { "Main site" }))
        .fallback(tenant_fallback)
        .with_state(state);
}
 
async fn tenant_fallback(
    State(state): State<Arc<AppState>>,
    Host(host): Host,
) -> impl IntoResponse {
    // Look up tenant by host
    match state.tenants.get(&host) {
        Some(_tenant_router) => {
            // In a real implementation, you'd route to the tenant router
            (StatusCode::OK, format!("Tenant found: {}", host))
        }
        None => {
            (StatusCode::NOT_FOUND, format!("Unknown tenant: {}", host))
        }
    }
}

The fallback can implement complex routing logic based on request properties.

When to Use Each

use axum::{Router, routing::get, response::IntoResponse, http::StatusCode};
use tower_http::services::ServeDir;
 
async fn usage_recommendations() {
    // Use fallback when:
    // - You need a simple 404 handler
    // - You want to use extractors (State, Path, Query, etc.)
    // - You need handler-like error handling
    
    let app1 = Router::new()
        .route("/api/data", get(|| async { "Data" }))
        .fallback(|uri| async move {
            (StatusCode::NOT_FOUND, format!("Not found: {}", uri))
        });
    
    // Use fallback_service when:
    // - Serving static files (SPA routing)
    // - Delegating to another router
    // - Integrating tower services
    // - You need service-specific middleware
    
    let app2 = Router::new()
        .route("/api/data", get(|| async { "Data" }))
        .fallback_service(ServeDir::new("public"));
    
    // Use fallback_service for router delegation:
    let api_router = Router::new()
        .route("/v2/data", get(|| async { "V2 Data" }));
    
    let legacy_router = Router::new()
        .route("/v1/data", get(|| async { "V1 Data" }));
    
    let app3 = Router::new()
        .merge(api_router)
        .fallback_service(legacy_router);
}

Choose based on whether you need handler simplicity or service flexibility.

Synthesis

use axum::{Router, routing::get, response::IntoResponse, http::StatusCode};
use tower_http::services::ServeDir;
 
async fn complete_guide_summary() {
    // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    // β”‚                        fallback vs fallback_service                     β”‚
    // β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    // β”‚ fallback:                                                               β”‚
    // β”‚   - Accepts handler functions (like route handlers)                   β”‚
    // β”‚   - Can use extractors (State, Path, Query, etc.)                     β”‚
    // β”‚   - Simpler syntax for response generation                            β”‚
    // β”‚   - Use for: custom 404 pages, dynamic error responses                β”‚
    // β”‚                                                                         β”‚
    // β”‚ fallback_service:                                                       β”‚
    // β”‚   - Accepts any Service<Request>                                       β”‚
    // β”‚   - Works with tower services and middleware                          β”‚
    // β”‚   - Enables router delegation and file serving                        β”‚
    // β”‚   - Use for: SPAs, static files, delegating to other routers          β”‚
    // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    
    // Common patterns:
    
    // Pattern 1: Custom 404 with request info
    let app1 = Router::new()
        .route("/api", get(|| async { "API" }))
        .fallback(|uri| async move {
            (StatusCode::NOT_FOUND, format!("No route for {}", uri))
        });
    
    // Pattern 2: SPA routing
    let app2 = Router::new()
        .route("/api/data", get(|| async { "Data" }))
        .fallback_service(ServeDir::new("dist"));
    
    // Pattern 3: Fallback with state for tenant routing
    let app3 = Router::new()
        .route("/", get(|| async { "Home" }))
        .fallback(tenant_handler);
}
 
async fn tenant_handler() -> impl IntoResponse {
    (StatusCode::NOT_FOUND, "Unknown tenant")
}
 
// Key insight:
// fallback and fallback_service achieve the same goalβ€”handling unmatched
// routesβ€”but accept different types. fallback is for handler functions with
// extractor support, while fallback_service is for tower services. The
// latter enables integration with ServeDir, other Routers, and any tower
// middleware stack. Choose based on your needs: handler simplicity vs.
// service flexibility.

Key insight: fallback and fallback_service are two sides of the same coinβ€”both handle unmatched requests, but they accept different abstractions. fallback takes handler functions with extractor support, making it ideal for custom error responses that need request context. fallback_service takes any Service, enabling integration with tower's ecosystem: static file serving (ServeDir), router delegation (nesting routers), and custom service implementations. For SPA routing, use fallback_service(ServeDir::new(...)) or ServeFile::new(...). For custom 404 handlers that need state or extractors, use fallback. The middleware behavior differs: router middleware applies to fallback, while fallback_service can have its own middleware stack via ServiceBuilder.