Contents

Axum Codec

axum_codec

axum_codec 是一个用于处理 axum 请求和响应的编解码器。 可用根据客户端的accept头,自动处理 JSON、XML、YAML、TOML、MsgPack格式。

#[axum_codec::apply(encode, decode)]
struct User {
  name: String,
  age: u8,
}

async fn me() -> Codec<User> {
  Codec(User {
    name: "Alice".into(),
    age: 42,
  })
}

通过包装成 Codec<User> 自动客户端请求的格式。

extract时错误处理

为了统一格式,我们需要在extract出错时,也返回客户端期望的格式,如msgpack。应该如何处理呢。 首先我们需要定义一个错误类型,如 AppError,并让它实现 IntoCodecResponse trait,然后如下

use axum::extract::{FromRequestParts, Path, Query};
use axum::http::request::Parts;
use axum::response::IntoResponse;
use axum_codec::IntoCodecResponse;

pub struct ApiErrorWithAccept {
    error: ApiError,
    accept: axum_codec::Accept,
}

impl IntoResponse for ApiErrorWithAccept {
    fn into_response(self) -> axum::response::Response {
        let ApiErrorWithAccept { error, accept } = self;
        let response = error.into_codec_response(accept.into());
        response.into_response()
    }
}

pub struct AppPath<T>(pub T);

impl<S, T> FromRequestParts<S> for AppPath<T>
where
    T: serde::de::DeserializeOwned + Send,
    S: Send + Sync + 'static,
{
    type Rejection = ApiErrorWithAccept;

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        let accept = axum_codec::Accept::from_request_parts(parts, state)
            .await
            .unwrap();

        match Path::<T>::from_request_parts(parts, state).await {
            Ok(Path(value)) => Ok(AppPath(value)),
            Err(rejection) => Err(ApiErrorWithAccept {
                error: ApiError::new(
                    ApiErrorCode::ParamOrQuery,
                    format!("Invalid path parameter: {}", rejection),
                ),
                accept,
            }),
        }
    }
}

实现 FromRequestParts,并自定义 Rejection 类型,使其中包含 ApiErroraxum_codec::Accept, 例如上面的 ApiErrorWithAccept,并为其实现 IntoResponse trait,也可通过相似的方法实现 AppQuery

404时返回格式

use axum_codec::{Accept, IntoCodecResponse};
use tower_http::trace::TraceLayer;

async fn not_found(accept: Accept) -> impl IntoResponse {
    ApiError::not_found().into_codec_response(accept.into())
}

let app = Router::new()
    .route("/health", get(health_check).into())
    .nest("/api/v1", api_v1)
    .fallback(not_found)
    .layer(TraceLayer::new_for_http().on_request(
        |req: &axum::http::Request<axum::body::Body>, _span: &tracing::Span| {
            info!("{} {}", req.method(), req.uri());
        },
    ));

accept: Accept,可以直接声明,自动extract,使用一个实现了 IntoCodecResponse 的对象, 转换时使用accept。最后在route中使用fallback即可。

上述代码最后面是用来打开每个请求的日志,方便调试。