Skip to content

Commit

Permalink
refactor: rename api errors, add clarifying comments and update README
Browse files Browse the repository at this point in the history
  • Loading branch information
koskeller committed Jun 6, 2024
1 parent 7ce4657 commit f0ff365
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 13 deletions.
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,45 @@ To format `.json` logs using [`jq`](https://github.com/jqlang/jq):
$ cargo watch -q -x run | jq .
```

## Example

```rust
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ExampleReq {
pub input: String,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ExampleResp {
pub output: String,
}

pub async fn example(
State(state): State<AppState>,
req: Result<Json<ExampleReq>, JsonRejection>,
) -> Result<Json<ExampleResp>, ApiError> {
// Returns ApiError::InvalidJsonBody if the Axum built-in extractor
// returns an error.
let Json(req) = req?;

// Proceed with additional validation.
if req.input.is_empty() {
return Err(ApiError::InvalidRequest(
"'input' should not be empty".to_string(),
));
}

// Anyhow errors are by default converted into ApiError::InternalError and assigned a 500 HTTP status code.
let data: anyhow::Result<()> = Err(anyhow!("Some internal error"));
let data = data?;

let resp = ExampleResp {
output: "hello".to_string(),
};
Ok(Json(resp))
}
```

## Contributing

Contributions are always welcome! Feel free to check the current issues in this repository for tasks that need attention. If you find something missing or that could be improved, please open a new issue.
Contributions are always welcome! Feel free to check the current issues in this repository for tasks that need attention. If you find something missing or that could be improved, please open a new issue.
40 changes: 28 additions & 12 deletions src/api_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,41 @@ use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::error;

/// Custom error type for the API.
/// The `#[from]` attribute allows for easy conversion from other error types.
#[derive(Error, Debug)]
pub enum ApiError {
#[error("Invalid request.")]
RequestValidation(#[from] JsonRejection),
/// Converts from an Axum built-in extractor error.
#[error("Invalid payload.")]
InvalidJsonBody(#[from] JsonRejection),

#[error("A database error occurred.")]
Database(#[from] sqlx::Error),
/// For errors that occur during manual validation.
#[error("Invalid request: {0}")]
InvalidRequest(String),

#[error("An internal server error occurred.")]
Internal(#[from] anyhow::Error),
/// Converts from `sqlx::Error`.
#[error("A database error has occurred.")]
DatabaseError(#[from] sqlx::Error),

/// Converts from any `anyhow::Error`.
#[error("An internal server error has occurred.")]
InternalError(#[from] anyhow::Error),
}

#[derive(Serialize, Deserialize)]
pub struct ApiErrorResp {
pub message: String,
}

// The IntoResponse implementation for ApiError logs the error message.
//
// To avoid exposing implementation details to API consumers, we separate
// the message that we log from the API response message.
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
// Log detailed error for telemetry.
let error_to_log = match &self {
ApiError::RequestValidation(ref err) => match err {
ApiError::InvalidJsonBody(ref err) => match err {
JsonRejection::JsonDataError(e) => e.body_text(),
JsonRejection::JsonSyntaxError(e) => e.body_text(),
JsonRejection::MissingJsonContentType(_) => {
Expand All @@ -38,20 +51,23 @@ impl IntoResponse for ApiError {
JsonRejection::BytesRejection(_) => "Failed to buffer request body".to_string(),
_ => "Unknown error".to_string(),
},
ApiError::Database(ref err) => format!("{}", err),
ApiError::Internal(ref err) => format!("{}", err),
ApiError::InvalidRequest(_) => format!("{}", self),
ApiError::DatabaseError(ref err) => format!("{}", err),
ApiError::InternalError(ref err) => format!("{}", err),
};
error!("{}", error_to_log);

// Create a general response to hide specific implementation details.
// Create a generic response to hide specific implementation details.
let resp = ApiErrorResp {
message: self.to_string(),
};

// Determine the appropriate status code.
let status = match self {
ApiError::RequestValidation(_) => StatusCode::BAD_REQUEST,
ApiError::Database(_) | ApiError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
ApiError::InvalidJsonBody(_) | ApiError::InvalidRequest(_) => StatusCode::BAD_REQUEST,
ApiError::DatabaseError(_) | ApiError::InternalError(_) => {
StatusCode::INTERNAL_SERVER_ERROR
}
};

(status, Json(resp)).into_response()
Expand Down

0 comments on commit f0ff365

Please sign in to comment.