AxumMiddleTechnical

Что такое FromRequest и FromRequestParts в Axum? Когда следует их реализовывать?

FromRequestParts извлекает данные только из заголовков/URI (не потребляет тело), FromRequest может читать тело запроса; первый следует использовать для аутентификации и параметров пути.

Разница между FromRequest и FromRequestParts

Axum разделяет экстракцию данных на два трейта в зависимости от того, нужен ли доступ к телу запроса:

  • FromRequestParts — работает только с Parts (метод, URI, заголовки, extensions). Тело не потребляется. Можно использовать несколько раз, в том числе в middleware.
  • FromRequest — получает полный Request включая тело. Тело можно прочитать только один раз, поэтому в сигнатуре хендлера должен быть не более одного экстрактора, реализующего FromRequest (например Json<T>, Bytes, String).

Реализация FromRequestParts — извлечение JWT из заголовка

use axum::{
    async_trait,
    extract::FromRequestParts,
    http::{request::Parts, StatusCode},
};

struct AuthUser {
    user_id: i64,
}

#[async_trait]
impl<S> FromRequestParts<S> for AuthUser
where
    S: Send + Sync,
{
    type Rejection = (StatusCode, &'static str);

    async fn from_request_parts(
        parts: &mut Parts,
        _state: &S,
    ) -> Result<Self, Self::Rejection> {
        let token = parts
            .headers
            .get("Authorization")
            .and_then(|v| v.to_str().ok())
            .and_then(|v| v.strip_prefix("Bearer "))
            .ok_or((StatusCode::UNAUTHORIZED, "missing token"))?;

        let user_id = verify_jwt(token)
            .map_err(|_| (StatusCode::UNAUTHORIZED, "invalid token"))?;

        Ok(AuthUser { user_id })
    }
}

async fn profile(user: AuthUser) -> String {
    format!("user_id = {}", user.user_id)
}

Реализация FromRequest — кастомный JSON с валидацией

use axum::{
    async_trait,
    extract::{FromRequest, Request},
    http::StatusCode,
    Json,
};
use serde::de::DeserializeOwned;
use validator::Validate;

struct ValidJson<T>(T);

#[async_trait]
impl<T, S> FromRequest<S> for ValidJson<T>
where
    T: DeserializeOwned + Validate,
    S: Send + Sync,
{
    type Rejection = (StatusCode, String);

    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
        let Json(value) = Json::<T>::from_request(req, state)
            .await
            .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;

        value.validate()
            .map_err(|e| (StatusCode::UNPROCESSABLE_ENTITY, e.to_string()))?;

        Ok(ValidJson(value))
    }
}

Доступ к State внутри экстрактора

Если экстрактору нужен доступ к состоянию приложения, используйте FromRef:

use axum::extract::{FromRef, FromRequestParts};

#[async_trait]
impl<S> FromRequestParts<S> for AuthUser
where
    AppState: FromRef<S>,
    S: Send + Sync,
{
    type Rejection = StatusCode;

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, StatusCode> {
        let app_state = AppState::from_ref(state);
        // используем app_state.jwt_secret для верификации
        todo!()
    }
}

Подводные камни

  • Если реализовать FromRequest там, где достаточно FromRequestParts, экстрактор нельзя использовать совместно с Json<T> — они оба потребуют тело и компилятор откажется.
  • async_trait по-прежнему требуется вплоть до стабилизации async fn in traits в Rust; без него компилятор выдаёт ошибку об object-safety.
  • Тип Rejection должен реализовывать IntoResponse; использование String напрямую не сработает — оберните в (StatusCode, String).
  • Порядок параметров в хендлере важен: FromRequestParts-экстракторы должны идти до FromRequest-экстракторов (Axum проверяет это статически).
  • Паника внутри from_request убьёт worker-поток Tokio; всегда возвращайте Err вместо panic!.
  • Не читайте body дважды: после того как тело потреблено одним экстрактором, другой получит пустые байты без ошибки.
  • При использовании axum::middleware::from_fn нельзя напрямую использовать экстракторы — нужно вызывать Parts::extract::<T>() вручную.
  • Изменения в Parts::extensions внутри экстрактора видны последующим экстракторам — это удобно для цепочки, но может породить неочевидные зависимости между хендлерами.

Common mistakes

  • Отвечать определением без production-сценария.
  • Не называть runtime boundary, security boundary или failure mode.
  • Игнорировать версию API, observability и тестовую проверку.

What the interviewer is testing

  • Объясняет механизм своими словами и без выдуманных API.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics