-
-
Notifications
You must be signed in to change notification settings - Fork 4
feat(server): Move to structured ApiError #263
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8dbf14f
dc092ec
977c22c
21743ad
a78ffa3
45f120f
4474c5c
cae2963
00b163b
1ab7d99
2e0fa94
d5a6910
5f0b58f
b371f32
38520c6
114ff04
0bd7e6c
417bb66
450283c
85f6ca9
158e6f7
a302811
9570515
d362507
eb7d880
74ef8e5
d6c55df
2455d66
ad928f5
c493fb4
a46d6cd
17c14ad
c2d154a
9dde12e
224563b
be9778c
418dc97
4be74ea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,42 +1,83 @@ | ||
| //! | ||
| //! This is mostly adapted from <https://github.com/tokio-rs/axum/blob/main/examples/anyhow-error-response/src/main.rs> | ||
| //! Common types and utilities for API endpoints. | ||
|
|
||
| use std::error::Error; | ||
|
|
||
| use axum::Json; | ||
| use axum::http::StatusCode; | ||
| use axum::response::{IntoResponse, Response}; | ||
| use objectstore_service::ServiceError; | ||
| use serde::{Deserialize, Serialize}; | ||
| use thiserror::Error; | ||
|
|
||
| use crate::auth::AuthError; | ||
|
|
||
| /// Error type for API operations. | ||
| #[derive(Debug, Error)] | ||
| pub enum ApiError { | ||
| /// Errors indicating malformed or illegal requests. | ||
| #[error("client error: {0}")] | ||
| Client(String), | ||
|
|
||
| pub enum AnyhowResponse { | ||
| Error(anyhow::Error), | ||
| Response(Response), | ||
| /// Authorization/authentication errors. | ||
| #[error("auth error: {0}")] | ||
| Auth(#[from] AuthError), | ||
|
|
||
| /// Service errors, indicating that something went wrong when receiving or executing a request. | ||
| #[error("service error: {0}")] | ||
| Service(#[from] ServiceError), | ||
| } | ||
|
|
||
| pub type ApiResult<T> = std::result::Result<T, AnyhowResponse>; | ||
| /// Result type for API operations. | ||
| pub type ApiResult<T> = Result<T, ApiError>; | ||
|
|
||
| impl IntoResponse for AnyhowResponse { | ||
| fn into_response(self) -> Response { | ||
| match self { | ||
| AnyhowResponse::Error(error) => { | ||
| tracing::error!( | ||
| error = error.as_ref() as &dyn std::error::Error, | ||
| "error handling request" | ||
| ); | ||
|
|
||
| // TODO: Support more nuanced return codes for validation errors etc. See | ||
| // Relay's ApiErrorResponse and BadStoreRequest as examples. | ||
| StatusCode::INTERNAL_SERVER_ERROR.into_response() | ||
| } | ||
| AnyhowResponse::Response(response) => response, | ||
| } | ||
| } | ||
| /// A JSON error response returned by the API. | ||
| #[derive(Serialize, Deserialize, Debug)] | ||
| pub struct ApiErrorResponse { | ||
| /// The main error message. | ||
| #[serde(default)] | ||
| detail: Option<String>, | ||
| /// Chain of error causes. | ||
| #[serde(default, skip_serializing_if = "Vec::is_empty")] | ||
| causes: Vec<String>, | ||
| } | ||
|
|
||
| impl From<Response> for AnyhowResponse { | ||
| fn from(response: Response) -> Self { | ||
| Self::Response(response) | ||
| impl ApiErrorResponse { | ||
| /// Creates an error response from an error, extracting the full cause chain. | ||
| pub fn from_error<E: Error + ?Sized>(error: &E) -> Self { | ||
| let detail = Some(error.to_string()); | ||
|
|
||
| let mut causes = Vec::new(); | ||
| let mut source = error.source(); | ||
| while let Some(s) = source { | ||
| causes.push(s.to_string()); | ||
| source = s.source(); | ||
| } | ||
|
|
||
| Self { detail, causes } | ||
| } | ||
| } | ||
|
|
||
| impl From<anyhow::Error> for AnyhowResponse { | ||
| fn from(err: anyhow::Error) -> Self { | ||
| Self::Error(err) | ||
| impl IntoResponse for ApiError { | ||
| fn into_response(self) -> Response { | ||
| let status = match &self { | ||
| ApiError::Client(_) => StatusCode::BAD_REQUEST, | ||
|
|
||
| ApiError::Auth(AuthError::BadRequest(_)) => StatusCode::BAD_REQUEST, | ||
| ApiError::Auth(AuthError::ValidationFailure(_)) | ||
| | ApiError::Auth(AuthError::VerificationFailure) => StatusCode::UNAUTHORIZED, | ||
| ApiError::Auth(AuthError::NotPermitted) => StatusCode::FORBIDDEN, | ||
| ApiError::Auth(AuthError::InternalError(_)) => { | ||
| tracing::error!(error = &self as &dyn Error, "auth system error"); | ||
| StatusCode::INTERNAL_SERVER_ERROR | ||
| } | ||
|
|
||
| ApiError::Service(_) => { | ||
| tracing::error!(error = &self as &dyn Error, "error handling request"); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This whole refactor will end up creating new issue groups in Sentry |
||
| StatusCode::INTERNAL_SERVER_ERROR | ||
| } | ||
| }; | ||
|
|
||
| let body = ApiErrorResponse::from_error(&self); | ||
| (status, Json(body)).into_response() | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,3 @@ | ||
| use anyhow::Context as _; | ||
| use std::io; | ||
| use std::time::SystemTime; | ||
|
|
||
|
|
@@ -9,6 +8,7 @@ use axum::response::{IntoResponse, Response}; | |
| use axum::routing; | ||
| use axum::{Json, Router}; | ||
| use futures_util::{StreamExt, TryStreamExt}; | ||
| use objectstore_service::ServiceError; | ||
| use objectstore_service::id::{ObjectContext, ObjectId}; | ||
| use objectstore_types::Metadata; | ||
| use serde::Serialize; | ||
|
|
@@ -46,8 +46,7 @@ async fn objects_post( | |
| headers: HeaderMap, | ||
| body: Body, | ||
| ) -> ApiResult<Response> { | ||
| let mut metadata = | ||
| Metadata::from_headers(&headers, "").context("extracting metadata from headers")?; | ||
| let mut metadata = Metadata::from_headers(&headers, "").map_err(ServiceError::from)?; | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here it's kinda weird that we're creating a |
||
| metadata.time_created = Some(SystemTime::now()); | ||
|
|
||
| let stream = body.into_data_stream().map_err(io::Error::other).boxed(); | ||
|
|
@@ -73,9 +72,7 @@ async fn object_get( | |
| }; | ||
| let stream = MeteredPayloadStream::from(stream, state.rate_limiter.bytes_accumulator()).boxed(); | ||
|
|
||
| let headers = metadata | ||
| .to_headers("", false) | ||
| .context("extracting metadata from headers")?; | ||
| let headers = metadata.to_headers("", false).map_err(ServiceError::from)?; | ||
| Ok((headers, Body::from_stream(stream)).into_response()) | ||
| } | ||
|
|
||
|
|
@@ -84,9 +81,7 @@ async fn object_head(service: AuthAwareService, Xt(id): Xt<ObjectId>) -> ApiResu | |
| return Ok(StatusCode::NOT_FOUND.into_response()); | ||
| }; | ||
|
|
||
| let headers = metadata | ||
| .to_headers("", false) | ||
| .context("extracting metadata from headers")?; | ||
| let headers = metadata.to_headers("", false).map_err(ServiceError::from)?; | ||
|
|
||
| Ok((StatusCode::NO_CONTENT, headers).into_response()) | ||
| } | ||
|
|
@@ -98,8 +93,7 @@ async fn object_put( | |
| headers: HeaderMap, | ||
| body: Body, | ||
| ) -> ApiResult<Response> { | ||
| let mut metadata = | ||
| Metadata::from_headers(&headers, "").context("extracting metadata from headers")?; | ||
| let mut metadata = Metadata::from_headers(&headers, "").map_err(ServiceError::from)?; | ||
| metadata.time_created = Some(SystemTime::now()); | ||
|
|
||
| let ObjectId { context, key } = id; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is now mostly adapted from Relay's
ApiErrorResponse: https://github.com/getsentry/relay/blob/88f4c53e98e407025168f01276e09b2681373c98/relay-server/src/utils/api.rs#L24