From 0977a5ffcbb965261c1e86f7c12164e8b46a2907 Mon Sep 17 00:00:00 2001 From: Timo Ley Date: Sun, 2 Jan 2022 17:25:23 +0100 Subject: [PATCH] Improve API error handling --- Cargo.toml | 3 ++- src/v1/error.rs | 45 ++++++++++++++++++++++++++------------------- src/v1/models.rs | 2 +- src/v1/routes.rs | 23 ++++++++++++----------- 4 files changed, 41 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8860a2c..288d6fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,4 +22,5 @@ new_mime_guess = "3.0.2" headers = "0.3.5" url = {version = "2.2.2", features = ["serde"]} askama = "0.10" -urlencoding = "2.1.0" \ No newline at end of file +urlencoding = "2.1.0" +thiserror = "1.0.30" \ No newline at end of file diff --git a/src/v1/error.rs b/src/v1/error.rs index 94daaab..b29a242 100644 --- a/src/v1/error.rs +++ b/src/v1/error.rs @@ -2,43 +2,50 @@ use std::convert::Infallible; use axum::{ body::{Bytes, Full}, + extract::multipart::MultipartError, response::IntoResponse, Json, }; use reqwest::StatusCode; +use thiserror::Error; use super::models::ErrorResponse; +#[derive(Error, Debug)] +pub enum APIError { + #[error("SQL error: {0}")] + Sql(#[from] sqlx::Error), + #[error("Multipart form error: {0}")] + Multipart(#[from] MultipartError), + #[error("Bad request: {0}")] + BadRequest(String), +} + impl ErrorResponse { fn new(status: StatusCode, message: Option) -> Self { - ErrorResponse { + let reason = status.canonical_reason().unwrap_or_default(); + Self { status, - error: message, + error: message.unwrap_or(reason.to_string()), } } } -impl IntoResponse for ErrorResponse { +impl IntoResponse for APIError { type Body = Full; type BodyError = Infallible; fn into_response(self) -> axum::http::Response { - let status = self.status.clone(); - (status, Json(self)).into_response() - } -} - -impl From for ErrorResponse { - fn from(err: sqlx::Error) -> Self { - match err { - sqlx::Error::RowNotFound => { - Self::new(StatusCode::NOT_FOUND, Some("Not found".to_string())) - } - _ => Self::new( - StatusCode::INTERNAL_SERVER_ERROR, - Some("Internal Server Error".to_string()), - ), - } + let res = match self { + APIError::Sql(err) => match err { + sqlx::Error::RowNotFound => ErrorResponse::new(StatusCode::NOT_FOUND, None), + _ => ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, None), + }, + APIError::Multipart(_) => ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, None), + APIError::BadRequest(err) => ErrorResponse::new(StatusCode::BAD_REQUEST, Some(err)), + }; + let status = res.status.clone(); + (status, Json(res)).into_response() } } diff --git a/src/v1/models.rs b/src/v1/models.rs index 7bfa3a3..0c0d46d 100644 --- a/src/v1/models.rs +++ b/src/v1/models.rs @@ -88,7 +88,7 @@ pub struct UploadResponse { pub struct ErrorResponse { #[serde(serialize_with = "serialize_status")] pub status: StatusCode, - pub error: Option, + pub error: String, } //Query diff --git a/src/v1/routes.rs b/src/v1/routes.rs index e13c891..974994c 100644 --- a/src/v1/routes.rs +++ b/src/v1/routes.rs @@ -1,4 +1,5 @@ use crate::config::ConfVars; +use crate::ipfs::IPFSFile; use crate::v1::models::*; use axum::extract::{ContentLengthLimit, Extension, Multipart, Query}; use axum::handler::{get, post}; @@ -7,11 +8,13 @@ use axum::routing::BoxRoute; use axum::{Json, Router}; use sqlx::MySqlPool; +use super::error::APIError; + async fn meme( params: Query, Extension(db_pool): Extension, Extension(vars): Extension, -) -> Result { +) -> Result { let meme = Meme::get(params.id, &db_pool, vars.cdn).await?; Ok(Json(MemeResponse { status: 200, @@ -24,7 +27,7 @@ async fn memes( params: Query, Extension(db_pool): Extension, Extension(vars): Extension, -) -> Result { +) -> Result { let memes = Meme::get_all(params.0, &db_pool, vars.cdn).await?; Ok(Json(MemesResponse { status: 200, @@ -36,7 +39,7 @@ async fn memes( async fn category( params: Query, Extension(db_pool): Extension, -) -> Result { +) -> Result { let category = Category::get(¶ms.id, &db_pool).await?; Ok(Json(CategoryResponse { status: 200, @@ -47,7 +50,7 @@ async fn category( async fn categories( Extension(db_pool): Extension, -) -> Result { +) -> Result { let categories = Category::get_all(&db_pool).await?; Ok(Json(CategoriesResponse { status: 200, @@ -59,7 +62,7 @@ async fn categories( async fn user( params: Query, Extension(db_pool): Extension, -) -> Result { +) -> Result { let user = User::get(params.0, &db_pool).await?; Ok(Json(UserResponse { status: 200, @@ -68,9 +71,7 @@ async fn user( })) } -async fn users( - Extension(db_pool): Extension, -) -> Result { +async fn users(Extension(db_pool): Extension) -> Result { let users = User::get_all(&db_pool).await?; Ok(Json(UsersResponse { status: 200, @@ -83,7 +84,7 @@ async fn random( params: Query, Extension(db_pool): Extension, Extension(vars): Extension, -) -> Result { +) -> Result { let random = Meme::get_random(params.0, &db_pool, vars.cdn).await?; Ok(Json(MemeResponse { status: 200, @@ -96,8 +97,8 @@ async fn upload( ContentLengthLimit(mut form): ContentLengthLimit, Extension(db_pool): Extension, Extension(vars): Extension, -) -> impl IntoResponse { - todo!(); +) -> Result { + Ok(()) } //TODO: Implement upload endpoint