슬기로운 개발자생활/Backend

Understanding Result, map_err, ?, Ok, and Err in Rust with Axum JWT Authentication

개발자 소신 2024. 2. 28. 21:55
반응형

Rust's powerful error handling model is exemplified by its use of the Result<T, E> type, which represents either success (Ok) or failure (Err). This model encourages explicit handling of all possible errors, making Rust programs more robust and reliable. In the context of web development with Axum, a web framework for Rust, understanding how to effectively use Result, map_err, ?, Ok, and Err is crucial for building secure and efficient web services. Let's dive into these concepts using the code snippet provided as a reference.

The Result<T, E> Type

In Rust, Result is an enum with two variants: Ok(T) and Err(E), where T represents the type of a successful outcome and E the type of an error. It's a core part of Rust's error handling:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Using Result in Axum Handlers

In our Axum handler named auth, the return type is Result<impl IntoResponse, (StatusCode, Json)>. This signifies that the function can either return a successful response that implements the IntoResponse trait or an error tuple consisting of a StatusCode and a Json response.

The ? Operator

The ? operator is used for error propagation. It returns the error part of a Result early if it is an Err, otherwise, it unwraps the Ok part, allowing the function to continue. This operator makes error handling more concise and readable.

Using map_err to Transform Errors

map_err is a method called on Result types that applies a function to the Err variant, effectively transforming the error into another form without touching the Ok variant. It's particularly useful when you need to change the error type to match the expected return type of a function or to enrich error information.

In the auth function, map_err is used multiple times to transform various errors into a uniform (StatusCode, Json) type, suitable for the function's return type. Each map_err call takes a closure that constructs the appropriate error response.

Handling Errors with Ok and Err

Ok and Err are constructors for Result variants. In the code, they are used both to propagate success states with Ok(next.run(req).await) and to return errors when, for example, token validation fails or a user cannot be found.

Practical Example from the Provided Code

Consider the token decoding part:

let claims = decode::<TokenClaims>(
    &token,
    &DecodingKey::from_secret(data.env.jwt_secret.as_ref()),
    &Validation::default(),
)
.map_err(|_| {
    let json_error = ErrorResponse {
        status: "fail",
        message: "Invalid token".to_string(),
    };
    (StatusCode::UNAUTHORIZED, Json(json_error))
})
.unwrap()
.claims; 

Here, decode attempts to decode the JWT token. If it fails (Err), map_err transforms this error into a uniform response type. The unwrap() call is a placeholder demonstrating a risky operation where we're assuming success; in production, you'd handle this more gracefully, typically with ? or more explicit error handling to avoid panics.

Conclusion

Understanding and effectively using Result, map_err, ?, Ok, and Err in Rust are crucial for writing robust, error-resistant applications. The provided code demonstrates how these constructs can be used in an Axum application to handle various error conditions gracefully, ensuring that the application behaves predictably and provides meaningful feedback to the client in case of errors.

By mastering these concepts, Rust developers can take full advantage of the language's powerful error handling model, leading to more reliable and maintainable codebases.

original source code

#[derive(Debug, Serialize)]
pub struct ErrorResponse {
    pub status: &'static str,
    pub message: String,
}

pub async fn auth(
    cookie_jar: CookieJar,
    State(data): State<Arc<AppState>>,
    mut req: Request<Body>,
    next: Next,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
    // logging

    let token = cookie_jar
        .get("token")
        .map(|cookie| cookie.value().to_string())
        .or_else(|| {
            req.headers()
                .get(header::AUTHORIZATION)
                .and_then(|auth_header| auth_header.to_str().ok())
                .and_then(|auth_value| {
                    if auth_value.starts_with("Bearer ") {
                        Some(auth_value[7..].to_owned())
                    } else {
                        None
                    }
                })
        });

    let token = token.ok_or_else(|| {
        let json_error = ErrorResponse {
            status: "fail",
            message: "You are not logged in, please provide token".to_string(),
        };
        (StatusCode::UNAUTHORIZED, Json(json_error))
    })?;

    let claims = decode::<TokenClaims>(
        &token,
        &DecodingKey::from_secret(data.env.jwt_secret.as_ref()),
        &Validation::default(),
    )
    .map_err(|_| {
        let json_error = ErrorResponse {
            status: "fail",
            message: "Invalid token".to_string(),
        };
        (StatusCode::UNAUTHORIZED, Json(json_error))
    })
    .unwrap()
    .claims;

    let user_id = ObjectId::parse_str(&claims.sub)
        .map_err(|_| {
            let json_error = ErrorResponse {
                status: "fail",
                message: "Invalid token".to_string(),
            };
            (StatusCode::UNAUTHORIZED, Json(json_error))
        })
        .unwrap();

    let user: User = find_one_user(user_id)
        .await
        .map_err(|_| {
            let json_error = ErrorResponse {
                status: "fail",
                message: "find user error".to_string(),
            };
            (StatusCode::UNAUTHORIZED, Json(json_error))
        })
        .unwrap();

    req.extensions_mut().insert(user);
    Ok(next.run(req).await)
}
반응형