Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions objectstore-server/src/endpoints/batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,7 @@ async fn batch(
let usecase = context.usecase.clone();
move |op| {
if let Operation::Insert(ins) = op {
state
.config
.usecases
.validate(&usecase, &ins.metadata)
.map_err(|e| ApiError::Client(e.to_string()))?;
state.config.usecases.validate(&usecase, &ins.metadata)?;
}
Ok(())
}
Expand Down
152 changes: 106 additions & 46 deletions objectstore-server/src/endpoints/common.rs
Original file line number Diff line number Diff line change
@@ -1,45 +1,19 @@
//! Common types and utilities for API endpoints.

use std::borrow::Cow;
use std::error::Error;

use axum::Json;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use http::HeaderValue;
use objectstore_service::error::Error as ServiceError;
use objectstore_service::error::{Error as ServiceError, ErrorKind as ServiceErrorKind};
use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::auth::AuthError;
use crate::extractors::batch::BatchError;

/// Error type for API operations.
#[derive(Debug, Error)]
pub enum ApiError {
/// Errors indicating malformed or illegal requests.
#[error("client error: {0}")]
Client(String),

/// 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),

/// Errors encountered when parsing or executing a batch request.
#[error("batch error: {0}")]
Batch(#[from] BatchError),

/// Internal server errors.
#[error("internal error: {0}")]
Internal(String),
}

/// Result type for API operations.
pub type ApiResult<T> = Result<T, ApiError>;

/// A JSON error response returned by the API.
#[derive(Serialize, Deserialize, Debug)]
pub struct ApiErrorResponse {
Expand Down Expand Up @@ -67,11 +41,77 @@ impl ApiErrorResponse {
}
}

/// Error type for API operations.
#[derive(Debug, Error)]
pub enum ApiError {
/// Errors indicating malformed or illegal requests.
#[error("client error: {context}")]
Client {
/// Context describing the operation that failed.
context: Cow<'static, str>,
/// The underlying error, if available.
#[source]
cause: Option<Box<dyn Error + Send + Sync>>,
},

/// 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),

/// Errors encountered when parsing or executing a batch request.
#[error("batch error: {0}")]
Batch(#[from] BatchError),

/// Internal server errors.
#[error("internal error: {context}")]
Internal {
/// Context describing the operation that failed.
context: Cow<'static, str>,
/// The underlying error, if available.
#[source]
cause: Option<Box<dyn Error + Send + Sync>>,
},
}

impl ApiError {
/// Creates a client error with context and an underlying cause.
pub fn map_client<E>(context: impl Into<Cow<'static, str>>, cause: E) -> Self
where
E: Error + Send + Sync + 'static,
{
Self::Client {
context: context.into(),
cause: Some(Box::new(cause)),
}
}

/// Creates a client error with context and no underlying cause.
pub fn client(context: impl Into<Cow<'static, str>>) -> Self {
Self::Client {
context: context.into(),
cause: None,
}
}

/// Creates an internal server error with context and an underlying cause.
pub fn internal<E>(context: impl Into<Cow<'static, str>>, cause: E) -> Self
where
E: Error + Send + Sync + 'static,
{
Self::Internal {
context: context.into(),
cause: Some(Box::new(cause)),
}
}

/// Returns the HTTP status code appropriate for this error variant.
pub fn status(&self) -> StatusCode {
match &self {
ApiError::Client(_) => StatusCode::BAD_REQUEST,
ApiError::Client { .. } => StatusCode::BAD_REQUEST,

ApiError::Batch(BatchError::BadRequest(_))
| ApiError::Batch(BatchError::Metadata(_))
Expand All @@ -90,23 +130,28 @@ impl ApiError {
ApiError::Auth(AuthError::NotPermitted) => StatusCode::FORBIDDEN,
ApiError::Auth(AuthError::InternalError(_)) => StatusCode::INTERNAL_SERVER_ERROR,

ApiError::Service(ServiceError::Client(_)) => StatusCode::BAD_REQUEST,
ApiError::Service(ServiceError::Metadata(_)) => StatusCode::BAD_REQUEST,
ApiError::Service(ServiceError::RangeNotSatisfiable { .. }) => {
StatusCode::RANGE_NOT_SATISFIABLE
}
ApiError::Service(ServiceError::InvalidUploadId(_)) => StatusCode::BAD_REQUEST,
ApiError::Service(ServiceError::UnknownUploadSession) => StatusCode::BAD_REQUEST,
ApiError::Service(ServiceError::ChunkExceedsUploadLength { .. }) => {
StatusCode::BAD_REQUEST
}
ApiError::Service(ServiceError::UploadOffsetMismatch { .. }) => StatusCode::CONFLICT,
ApiError::Service(ServiceError::UploadSessionGone) => StatusCode::GONE,
ApiError::Service(ServiceError::AtCapacity) => StatusCode::TOO_MANY_REQUESTS,
ApiError::Service(ServiceError::NotImplemented) => StatusCode::NOT_IMPLEMENTED,
ApiError::Service(_) => StatusCode::INTERNAL_SERVER_ERROR,

ApiError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
ApiError::Service(error) => match error.kind() {
ServiceErrorKind::InvalidMetadata
| ServiceErrorKind::InvalidUploadId
| ServiceErrorKind::ClientStream
| ServiceErrorKind::UnknownUploadSession
| ServiceErrorKind::ChunkExceedsUploadLength { .. } => StatusCode::BAD_REQUEST,
ServiceErrorKind::RangeNotSatisfiable { .. } => StatusCode::RANGE_NOT_SATISFIABLE,
ServiceErrorKind::UploadOffsetMismatch { .. } => StatusCode::CONFLICT,
ServiceErrorKind::UploadSessionGone => StatusCode::GONE,
ServiceErrorKind::AtCapacity => StatusCode::TOO_MANY_REQUESTS,
Comment thread
lcian marked this conversation as resolved.
ServiceErrorKind::Unsupported => StatusCode::NOT_IMPLEMENTED,
ServiceErrorKind::BackendRateLimited => StatusCode::TOO_MANY_REQUESTS,
ServiceErrorKind::BackendTimeout | ServiceErrorKind::BackendUnavailable => {
StatusCode::SERVICE_UNAVAILABLE
}
ServiceErrorKind::BackendFailure
| ServiceErrorKind::CorruptData
| ServiceErrorKind::Panic
| ServiceErrorKind::Internal => StatusCode::INTERNAL_SERVER_ERROR,
},

ApiError::Internal { .. } => StatusCode::INTERNAL_SERVER_ERROR,
}
}

Expand Down Expand Up @@ -134,6 +179,21 @@ impl IntoResponse for ApiError {
}
}

impl From<crate::usecases::UseCaseError> for ApiError {
fn from(error: crate::usecases::UseCaseError) -> Self {
ApiError::map_client("use case policy violation", error)
}
}

impl From<objectstore_types::metadata::Error> for ApiError {
fn from(error: objectstore_types::metadata::Error) -> Self {
ApiError::map_client("invalid metadata", error)
}
}

/// Result type for API operations.
pub type ApiResult<T> = Result<T, ApiError>;

/// Inserts `Accept-Ranges: bytes` into the response headers.
pub fn insert_accept_ranges(response: &mut Response) {
response.headers_mut().insert(
Expand Down
8 changes: 3 additions & 5 deletions objectstore-server/src/endpoints/multipart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ use bytes::Bytes;
use futures::StreamExt;
use http::HeaderValue;
use http::header;
use objectstore_service::error::Error as ServiceError;
use objectstore_service::id::{ObjectContext, ObjectId};
use objectstore_service::multipart::{CompletedPart, PartNumber, UploadId};
use objectstore_types::metadata::Metadata;
Expand Down Expand Up @@ -96,13 +95,12 @@ async fn initiate_inner(
headers: HeaderMap,
) -> ApiResult<Response> {
// TODO: Update time_created in `complete`, when we have a Service API to mutate metadata.
let metadata = Metadata::from_insert_headers(&headers, "").map_err(ServiceError::from)?;
let metadata = Metadata::from_insert_headers(&headers, "")?;

state
.config
.usecases
.validate(&id.context().usecase, &metadata)
.map_err(|e| ApiError::Client(e.to_string()))?;
.validate(&id.context().usecase, &metadata)?;

let upload_id = service.initiate_multipart(id.clone(), metadata).await?;

Expand All @@ -125,7 +123,7 @@ async fn upload_part(
.get(header::CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok())
.ok_or_else(|| ApiError::Client("Content-Length header is required".into()))?;
.ok_or_else(|| ApiError::client("content-length header is required"))?;

let content_md5 = headers
.get("content-md5")
Expand Down
47 changes: 26 additions & 21 deletions objectstore-server/src/endpoints/objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::routing;
use axum::{Json, Router};
use objectstore_service::error::Error as ServiceError;
use objectstore_service::error::ErrorKind;
use objectstore_service::id::{ObjectContext, ObjectId};
use objectstore_types::headers::ExtValue;
use objectstore_types::metadata::Metadata;
Expand Down Expand Up @@ -89,13 +89,12 @@ async fn create_object(
headers: HeaderMap,
MeteredBody(body): MeteredBody,
) -> ApiResult<Response> {
let metadata = Metadata::from_insert_headers(&headers, "").map_err(ServiceError::from)?;
let metadata = Metadata::from_insert_headers(&headers, "")?;

state
.config
.usecases
.validate(&context.usecase, &metadata)
.map_err(|e| ApiError::Client(e.to_string()))?;
.validate(&context.usecase, &metadata)?;

let response_id = service.insert_object(context, None, metadata, body).await?;
let response = Json(InsertObjectResponse {
Expand All @@ -118,23 +117,28 @@ async fn object_get(
let (metadata, content_range, stream) = match result {
Ok(Some(result)) => result,
Ok(None) => return Ok(StatusCode::NOT_FOUND.into_response()),
Err(ApiError::Service(ServiceError::RangeNotSatisfiable { total })) => {
let mut response = (
StatusCode::RANGE_NOT_SATISFIABLE,
[(
http::header::CONTENT_RANGE,
ContentRange::unsatisfiable_total_to_header_value(total),
)],
)
.into_response();
insert_accept_ranges(&mut response);
return Ok(response);
}
Err(ApiError::Service(e)) => match e.kind() {
ErrorKind::RangeNotSatisfiable { total } => {
let mut response = (
StatusCode::RANGE_NOT_SATISFIABLE,
[(
http::header::CONTENT_RANGE,
ContentRange::unsatisfiable_total_to_header_value(total),
)],
)
.into_response();
insert_accept_ranges(&mut response);
return Ok(response);
}
_ => return Err(e.into()),
Comment thread
sentry-warden[bot] marked this conversation as resolved.
},
Err(e) => return Err(e),
};

let stream = state.meter_stream(stream, &context);
let mut metadata_headers = metadata.to_headers("").map_err(ServiceError::from)?;
let mut metadata_headers = metadata
.to_headers("")
.map_err(|error| ApiError::internal("encoding object response metadata", error))?;

let mut response = match content_range {
Some(ref content_range) => {
Expand Down Expand Up @@ -168,7 +172,9 @@ async fn object_head(service: AuthAwareService, Xt(id): Xt<ObjectId>) -> ApiResu
return Ok(StatusCode::NOT_FOUND.into_response());
};

let mut headers = metadata.to_headers("").map_err(ServiceError::from)?;
let mut headers = metadata
.to_headers("")
.map_err(|error| ApiError::internal("encoding object response metadata", error))?;
insert_content_length(&mut headers, &metadata);

let mut response = (StatusCode::OK, headers).into_response();
Expand Down Expand Up @@ -245,15 +251,14 @@ async fn insert_object(
headers: HeaderMap,
MeteredBody(body): MeteredBody,
) -> ApiResult<Response> {
let metadata = Metadata::from_insert_headers(&headers, "").map_err(ServiceError::from)?;
let metadata = Metadata::from_insert_headers(&headers, "")?;

let ObjectId { context, key } = id;

state
.config
.usecases
.validate(&context.usecase, &metadata)
.map_err(|e| ApiError::Client(e.to_string()))?;
.validate(&context.usecase, &metadata)?;

let response_id = service
.insert_object(context, Some(key), metadata, body)
Expand Down
Loading
Loading