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.
