ActixMiddleTechnical
Как Actix-web обрабатывает конфликты маршрутов и каков приоритет сопоставления маршрутов?
Actix-web применяет маршруты в порядке регистрации — первый совпавший побеждает. Статические сегменты должны регистрироваться перед параметрическими (/users/me до /users/{id}), иначе специфичный маршрут никогда не будет достигнут.
Приоритет маршрутов и конфликты в Actix-web
Actix-web использует детерминированный алгоритм сопоставления маршрутов, основанный на порядке регистрации и специфичности паттерна. В отличие от некоторых других фреймворков, здесь нет автоматического ранжирования по «точности» — важен порядок, в котором маршруты добавлены.
Базовое правило: первый совпавший побеждает
use actix_web::{web, App, HttpResponse, HttpServer};
async fn specific() -> HttpResponse {
HttpResponse::Ok().body("specific route")
}
async fn dynamic(path: web::Path<String>) -> HttpResponse {
HttpResponse::Ok().body(format!("dynamic: {}", path))
}
// Правильный порядок: более специфичные маршруты сначала
App::new()
.route("/users/me", web::get().to(specific)) // 1. статический
.route("/users/{id}", web::get().to(dynamic)) // 2. с параметром
Scope и вложенные маршруты
Маршруты внутри web::scope() проверяются в порядке добавления. Scope сам сопоставляется первым по префиксу:
App::new()
.service(
web::scope("/api/v1")
.route("/health", web::get().to(health)) // /api/v1/health
.route("/jobs/facets", web::get().to(facets)) // /api/v1/jobs/facets
.route("/jobs/{id}", web::get().to(job)) // /api/v1/jobs/{id}
)
.service(
web::scope("/api/v2")
.route("/jobs", web::get().to(jobs_v2))
)
Статические сегменты vs параметры vs wildcards
Actix-web приоритизирует сопоставление следующим образом:
- Точное совпадение (
/users/me) — высший приоритет в пределах того же scope - Параметр (
/users/{id}) — совпадает с любым непустым сегментом без/ - Хвостовой wildcard (
/files/{path:.*}) — совпадает со всем оставшимся путём включая/
Пример конфликта и его решение
// НЕПРАВИЛЬНО: /users/me никогда не будет достигнут
App::new()
.route("/users/{id}", web::get().to(dynamic)) // поглощает /users/me
.route("/users/me", web::get().to(specific)) // мёртвый маршрут
// ПРАВИЛЬНО: специфичный маршрут первым
App::new()
.route("/users/me", web::get().to(specific)) // проверяется первым
.route("/users/{id}", web::get().to(dynamic)) // для остальных
Resource vs route — использование web::resource()
// web::resource позволяет навешивать несколько методов на один путь
App::new()
.service(
web::resource("/users/{id}")
.route(web::get().to(get_user))
.route(web::put().to(update_user))
.route(web::delete().to(delete_user))
)
Хвостовые wildcards
// {tail:.*} совпадает с любым суффиксом включая /
async fn serve_file(path: web::Path<String>) -> HttpResponse {
HttpResponse::Ok().body(format!("file: {}", path))
}
App::new()
.route("/static/{tail:.*}", web::get().to(serve_file))
// /static/css/main.css → path = "css/main.css"
Регулярные выражения в параметрах
// Параметр только для числовых ID
App::new()
.route("/users/{id:\\d+}", web::get().to(get_user_by_id))
.route("/users/{slug}", web::get().to(get_user_by_slug))
Подводные камни
- Порядок не очевиден при рефакторинге — перестановка маршрутов при добавлении нового может молча сломать уже работающий маршрут.
- scope() с trailing slash —
web::scope("/api")иweb::scope("/api/")ведут себя по-разному; первый совпадает с/apiи/api/..., второй требует строгий слэш. - Параметры не совпадают через / —
{id}не захватитa/b; для этого нужен{path:.*}. - Несколько методов через route() без resource() — при использовании нескольких
.route()с одним путём, но разными методами, они регистрируются как независимые маршруты; конфликтов нет, но это менее эффективно, чемweb::resource(). - Маршруты из разных плагинов/модулей — при динамической сборке App из нескольких модулей порядок добавления определяется порядком вызовов; неочевидные конфликты трудно отлаживать.
- Отсутствие предупреждений о недостижимых маршрутах — Actix-web не предупреждает о мёртвых маршрутах; единственный способ обнаружить — тесты или внимательный code review.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.