rust axum cook book1
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