actix_cors を使っていてもミドルウェアから Err を返すと CORS に引っかかる

ふとしです。

要はハンドラからのレスポンスと違って、ミドルウェアから Err を返すようなエラーは基本回復不能な「失敗」としてケアしません、回復可能なエラーなら Ok を返してねということらしいです。

A middlware error is indicative of a fatal failure. All middlwares should return Ok(...) responses if the error is recoverable. This is a different expectation to handlers.

ひっかかったミドルウェア

このブログの編集周りの処理で、ヘッダーの JWT を検証して期限切れならそこでハネるみたいな処理でした。

match di.authing().verify(token).await {
    Ok(_) => service.call(req).await,
    Err(e) => Err(error::ErrorUnauthorized("expired")),
}

Nuxt のサーバーサイドアクセスでは 401 かつ expired と判断できるのでトークンのリフレッシュをかけることができて問題はありませんでした。しかしクライアント側で期限切れを迎えると上の挙動から CORS にひっかかり単なる Network Error としか認識できず、リフレッシュの経路に入れずエラーになってて気づいたわけです。

対応

ミドルウェアのレスポンスの型を確定する

独自で返すのはエラーだけだったので MiddlewareBody の関連型 ResponseService の関連型におまかせで不確定にしていました。

impl<DI, S> Service<ServiceRequest> for MiddlewareBody<DI, S>
where
    DI: Di,
    S: Service<ServiceRequest, Error = actix_web::Error>,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;

今回は処理中で type Response を作成しなければならないため、具体的な型を指定します。actix_web::App に合わせて ServiceResponse を指定します。

impl<DI, S> Service<ServiceRequest> for MiddlewareBody<DI, S>
where
    DI: Di,
    S: Service<ServiceRequest, Error = actix_web::Error, Response = ServiceResponse>,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;

レスポンスを Ok で返す

これで返すべき型が確定するので、call から Ok かつ 401 のレスポンスを返すようにします。

match di.authing().verify(token).await {
    Ok(_) => service.call(req).await,
    Err(e) => {
            let (req, _) = req.into_parts();
            Ok(ServiceResponse::new(
                req,
                HttpResponse::Unauthorized().body("expired"),
            ))
        }
    }
}

これでミドルウェアで中断しても CORS 対応が行われるようになり、ブラウザからのアクセスからでも 401 であることが判別できるようになりました。

まとめ

今回は Nuxt 側でブラウザではないアクセスがあったので原因がすぐわかりました。

しかし Internal Server Error などでこの手のミドルウェアが働かず CORS エラーとして表示されてなかなか本当の原因がわからないということはよくありますね。いままで大丈夫だったのに CORS エラーが出た場合は、まず curl などで確認してみる習慣をつけたいですね。(Chrome の dev tool から簡単にヘッダー付きの curl を生成できたりしますし)