🫥

rust axum cook book1

2023/12/21に公開

Handler

  • text/plainで 固定の文字列を返したい
  • status codeを変えたい
  • response ヘッダを追加したい
  • jsonでpostされた内容をstructをbindしたい
    • jsonとstructが完全に合わなかった場合文字でエラーがでるが、これをカスタマイズしたい()

text/plainで 固定の文字列を返したい

async fn handler() -> &'static str {
    "Hello, world!"
}

固定のhtmlファイルを返したい

async fn handler() -> Html<&'static str> {
    Html(include_str!("../views/index.html"))
}

status codeを変えたい / response headerを変えたい/追加したい

impl IntoResponseで返すことで、細かく制御できます
CustomErrorの構造体定義は省略

async fn status400(params: Json<Value>) -> impl IntoResponse {
let builder = Response::builder();

    let body = Json(json!(CustomError {
            message: "app_name is required".to_string()
        }));
    builder
        .status(StatusCode::BAD_REQUEST)
        .header("Content-type", "application/json")
        .body(body)
        .unwrap()
	.into_body()
}

JSONを受け取りstructにバインドしたい

pub async fn test4(Json(params): Json<HomeResponse>) -> Result<Json<HomeResponse>> {
    format::json(params)
}

Formを受け取りstructにバインドしたい

レスポンスはjsonで返します

pub async fn test4(Form(params): Json<HomeResponse>) -> Result<Json<HomeResponse>> {
    format::json(params)
}

自動でstructを受け取るとエラーメッセージが固定なのでカスタマイズしたい

Optionでないフィールドがpostされたjsonに含まれていなかったり、型が違った場合、

Failed to deserialize the JSON body into the target type: app_name: invalid type: integer 10, expected a string at line 1 column 14

などのメッセージが返されてしまいます。

これをカスタマイズするには

より汎用的な型で受け取り、自分でstructに紐づければ良いです。

fn create_response(status: StatusCode, value: serde_json::Value) -> impl IntoResponse {
    let builder = Response::builder();

    let body = serde_json::to_string(&value).unwrap();

    builder
        .status(status)
        .header("Content-type", "application/json")
        .body(body)
        .unwrap()
}
async fn current3(params: Json<Value>) -> impl IntoResponse {
    let value = serde_json::from_value::<HomeResponse>(params.0);
    if &value.is_ok() == &true {
        let v = json!(value.unwrap());
        create_response(StatusCode::OK, v)
    } else {
        create_response(
            StatusCode::BAD_REQUEST,
            json!(CustomError {
                message: value.err().unwrap().to_string()
            }),
        )
    }
}

この方法であれば、自分でカスタマイズしたエラーを返すことができます.

他にも FromRequest traitを実装しておく方法もあります。

#[async_trait]
impl<B> FromRequest<B> for HomeResponse {
    type Rejection = Response<Body>;

    async fn from_request(req: Request, state: &B) -> std::result::Result<Self, Self::Rejection> {
        let app_name = req
            .headers()
            .get("app_name")
            .and_then(|value| value.to_str().ok())
            .map(|value| value.to_string());
        let error = CustomError {
            message: "app_name is required".to_string(),
        };
        Err(format::json(error).into_response())
    }
}

or

pub async fn echo(query: Result<Query<StartRequestQuery>, QueryRejection>) -> impl IntoResponse {
    println!("{:?}", &query);
    if query.is_err() {
        return format::json(CommonError{
            resultstatus: -1,
            reason: "parameter error".to_string(),
            servertime: 0,
        });
    }
    let q = query.unwrap().0;
    format::json(q)
}

QueryをResultで受取り、 Error側をQueryRejectionにします。 Query以外の場合は適宜XXRejectionを使います。
queryのエラーチェックで errの場合はResponseをreturnします。

JsonRejectionを使う方法もありそうです。

DB poolやステートなどを渡す

  • .layer(Extension(xxx))で layerを使ってわたし、 Extension(ex): Extension<XXX> で受け取る。
  • Stateでわたし State(s): State<XXXX> で受け取る

handlerのエラーをわかりやすくする

#[debug_handler]
をhandlerの宣言時につける

trait境界エラーが発生する

#[debug_handler]をつける
StateやExtensionでエラーが発生する
awaitを使うとエラーが発生する

mutex.lock();
xxx.await;
hoge_mutex.lock();
yyy.await;
などしていないだろうか?
awaitは事前にまとめて済ませよう。

Custom Extractor

リクエストが暗号化されている場合など カスタムextractorを作りたい時がある。

use std::f64::consts::E;

use bytes::Bytes;

use async_trait::async_trait;
use axum::body::Body;
use axum::extract::{ FromRequest, Json, Request};
use axum::http::request::{self, Parts};
use axum::http::StatusCode;
use serde::de::DeserializeOwned;
use serde_json::{to_string, Value};
#[derive(Debug, Clone, Copy, Default)]
pub struct XXEncrypt<T>(pub T);
#[async_trait]
impl<T, S> FromRequest<S> for XXEncrypt<T>
where
    T: DeserializeOwned,
    S: Send + Sync,
{
    type Rejection = (StatusCode, axum::Json<Value>);

    async fn from_request(req: Request<Body>, state: &S) -> Result<Self, Self::Rejection> {
        let (parts, body) = req.into_parts();
        let req = Request::from_parts(parts, body);
        let bt = Bytes::from_request(req, state).await.map_err(|e| {
            (
                StatusCode::BAD_REQUEST,
                axum::Json(Value::String(format!(
                    "request bytes error {}",
                    e.to_string()
                ))),
            )
        })?;
        let b: Vec<u8> = bt.as_ref().to_vec();
        let s = b.iter().map(|&x| x as char).collect::<String>();
        print!("{:?}", &bt);
        serde_json::from_str(s.as_str()).map_err(|e| {
            (
                StatusCode::BAD_REQUEST,
                axum::Json(Value::String(format!("unknown error {}", e.to_string()))),
            )
        })?;

        let json: T = serde_json::from_str(s.as_str()).map_err({
            |e| {
                (
                    StatusCode::BAD_REQUEST,
                    axum::Json(Value::String(format!("unknown error {}", e.to_string()))),
                )
            }
        })?;
        Ok(XXEncrypt(json))
    }
}

勘所は
type Rejection = (StatusCode, axum::Json<Value>); (Json出ない場合は適宜変更)
これと
let req = Request::from_parts(parts, body);
これでrequest型を作り出すところ(cloneできないので、このようにしないと他のExtractorを使えません)

(面倒だったので暗号化復号処理は端折りました)

Discussion