Skip to main content

shardline_server/
error.rs

1use std::{io::Error as IoError, num::TryFromIntError};
2
3use axum::{
4    Error as AxumError, Json,
5    http::{HeaderValue, StatusCode, header::WWW_AUTHENTICATE},
6    response::{IntoResponse, Response},
7};
8use serde::Serialize;
9use serde_json::Error as JsonError;
10use shardline_cache::ReconstructionCacheError;
11use shardline_index::{
12    FileRecordInvariantError, LocalIndexStoreError, MemoryIndexStoreError, MemoryRecordStoreError,
13    PostgresMetadataStoreError, QuarantineCandidateError, RetentionHoldError, WebhookDeliveryError,
14};
15use shardline_protocol::{HashParseError, HttpRangeParseError, TokenCodecError};
16pub use shardline_server_core::{
17    InvalidLifecycleMetadataError, InvalidReconstructionResponseError, InvalidSerializedShardError,
18    ParseStoredFileRecordError,
19};
20use shardline_storage::{LocalObjectStoreError, ObjectPrefixError, S3ObjectStoreError};
21use thiserror::Error;
22use tokio::task::JoinError;
23
24use crate::{
25    config::ServerConfigError, provider::ProviderServiceError, xet_adapter::XorbParseError,
26};
27use shardline_gc::GcError;
28use shardline_oci_adapter::OciAdapterError;
29use shardline_provider_events::ProviderEventsError;
30use shardline_xet_adapter::XetAdapterError;
31
32/// Object-storage subsystem failure.
33#[derive(Debug, Error)]
34pub enum ObjectStoreError {
35    /// Local storage IO failed.
36    #[error("local storage operation failed")]
37    Local(#[from] LocalObjectStoreError),
38    /// S3-compatible object-storage adapter access failed.
39    #[error("s3 object storage adapter operation failed")]
40    S3(#[from] S3ObjectStoreError),
41    /// Object inventory prefix validation failed.
42    #[error("object storage prefix validation failed")]
43    Prefix(#[from] ObjectPrefixError),
44    /// S3-compatible object storage was selected without concrete configuration.
45    #[error("s3 object storage configuration is missing")]
46    MissingS3Config,
47    /// Stored object metadata disagreed with the expected transfer length.
48    #[error("stored object length did not match indexed metadata")]
49    StoredLengthMismatch,
50    /// Storage migration found a content-addressed source object under the wrong key.
51    #[error(
52        "storage migration source object hash mismatch for key {key}: expected {expected_hash}, observed {observed_hash}"
53    )]
54    MigrationSourceHashMismatch {
55        /// Content-addressed object key being migrated.
56        key: String,
57        /// Hash implied by the object key.
58        expected_hash: String,
59        /// Hash computed from the source object bytes.
60        observed_hash: String,
61    },
62}
63
64/// Metadata index subsystem failure.
65#[derive(Debug, Error)]
66pub enum IndexError {
67    /// Index adapter access failed.
68    #[error("index adapter operation failed")]
69    Local(#[from] LocalIndexStoreError),
70    /// In-memory index adapter access failed.
71    #[error("memory index adapter operation failed")]
72    MemoryIndex(#[from] MemoryIndexStoreError),
73    /// In-memory record adapter access failed.
74    #[error("memory record adapter operation failed")]
75    MemoryRecord(#[from] MemoryRecordStoreError),
76    /// Postgres metadata adapter access failed.
77    #[error("postgres metadata adapter operation failed")]
78    PostgresMetadata(#[from] PostgresMetadataStoreError),
79    /// Retention hold input was invalid.
80    #[error("retention hold input was invalid")]
81    RetentionHold(#[from] RetentionHoldError),
82    /// Quarantine candidate input was invalid.
83    #[error("quarantine candidate input was invalid")]
84    QuarantineCandidate(#[from] QuarantineCandidateError),
85    /// Webhook delivery metadata was invalid.
86    #[error("webhook delivery metadata was invalid")]
87    WebhookDelivery(#[from] WebhookDeliveryError),
88    /// Stored file metadata could not produce a valid reconstruction plan.
89    #[error("stored file metadata was invalid")]
90    FileRecordInvariant(#[from] FileRecordInvariantError),
91    /// Lifecycle metadata was internally inconsistent.
92    #[error("lifecycle metadata was internally inconsistent")]
93    InvalidLifecycleMetadata(#[from] InvalidLifecycleMetadataError),
94    /// A reconstruction response violated an internal protocol-shape invariant.
95    #[error("reconstruction response invariant failed: {0}")]
96    InvalidReconstructionResponse(#[from] InvalidReconstructionResponseError),
97    /// A required metadata table was missing.
98    #[error("required metadata table is missing: {0}")]
99    MissingRequiredMetadataTable(String),
100    /// Repository rename encountered conflicting target-scope metadata.
101    #[error("repository rename target already contains conflicting metadata")]
102    ConflictingRenameTargetRecord,
103}
104
105/// Server runtime failure.
106///
107/// This is the unified error type for the Shardline server layer. It maps
108/// domain-specific errors from storage, indexing, protocol, auth, and OCI
109/// subsystems into a single enum with HTTP status code mapping.
110///
111/// # Design
112///
113/// Each variant maps to exactly one HTTP status code via [`ServerError::status_code`].
114/// Subsystem errors (e.g., [`LocalIndexStoreError`], [`S3ObjectStoreError`]) are
115/// converted via `From` implementations for ergonomic `?` usage.
116///
117/// When adding new error sources, prefer adding a new variant with a `#[from]`
118/// attribute over manual `From` implementations unless the source error maps
119/// to multiple variants.
120#[derive(Debug, Error)]
121pub enum ServerError {
122    /// Local storage IO failed.
123    #[error("local storage operation failed")]
124    Io(#[from] IoError),
125    /// JSON serialization or deserialization failed.
126    #[error("json operation failed")]
127    Json(#[from] JsonError),
128    /// Request body streaming failed.
129    #[error("request body stream failed")]
130    RequestBodyRead(#[source] AxumError),
131    /// Request body exceeded the configured maximum accepted byte count.
132    #[error("request body exceeded the configured maximum accepted byte count")]
133    RequestBodyTooLarge,
134    /// Request query exceeded the bounded metadata parser budget.
135    #[error("request query exceeded the bounded metadata parser budget")]
136    RequestQueryTooLarge,
137    /// Request body frame slicing exceeded checked bounds.
138    #[error("request body frame exceeded checked bounds")]
139    RequestBodyFrameOutOfBounds,
140    /// Numeric conversion exceeded supported bounds.
141    #[error("numeric conversion exceeded supported bounds")]
142    NumericConversion(#[from] TryFromIntError),
143    /// Hash parsing failed.
144    #[error("invalid content hash")]
145    HashParse(#[from] HashParseError),
146    /// Object-storage subsystem failure.
147    #[error("object storage operation failed")]
148    ObjectStore(#[from] ObjectStoreError),
149    /// Metadata index subsystem failure.
150    #[error("index adapter operation failed")]
151    Index(#[from] IndexError),
152    /// Stored file metadata exceeded the bounded parser ceiling.
153    #[error("stored file metadata exceeded the bounded parser ceiling")]
154    StoredFileMetadataTooLarge {
155        /// Observed file length in bytes.
156        observed_bytes: u64,
157        /// Maximum accepted file length in bytes.
158        maximum_bytes: u64,
159    },
160    /// Stored file metadata changed after bounded validation.
161    #[error("stored file metadata length did not match the validated length")]
162    StoredFileMetadataLengthMismatch,
163    /// A file identifier was unsafe.
164    #[error(
165        "file identifier must be relative and must not contain traversal or control characters"
166    )]
167    InvalidFileId,
168    /// A content hash was malformed.
169    #[error("content hash must be 64 hexadecimal characters")]
170    InvalidContentHash,
171    /// The xorb transfer path prefix was unsupported.
172    #[error("xorb transfer prefix must be default")]
173    InvalidXorbPrefix,
174    /// The uploaded xorb bytes did not match the requested hash.
175    #[error("xorb body hash did not match the requested path hash")]
176    XorbHashMismatch,
177    /// The uploaded xorb bytes were not a valid serialized xorb object.
178    #[error("xorb body was not a valid serialized xorb object")]
179    InvalidSerializedXorb,
180    /// The uploaded shard bytes were not a valid serialized shard object.
181    #[error("shard body was not a valid serialized shard object")]
182    InvalidSerializedShard(#[from] InvalidSerializedShardError),
183    /// A shard upload referenced a missing xorb.
184    #[error("shard referenced a missing xorb")]
185    MissingReferencedXorb,
186    /// Shard metadata exceeded bounded parser safety limits.
187    #[error("shard metadata exceeded bounded parser safety limits")]
188    TooManyShardTerms,
189    /// Batch reconstruction requested too many file identifiers.
190    #[error("batch reconstruction requested too many file identifiers")]
191    TooManyBatchReconstructionFileIds,
192    /// Requested content was not found.
193    #[error("content not found")]
194    NotFound,
195    /// Arithmetic overflowed a checked bound.
196    #[error("arithmetic overflow")]
197    Overflow,
198    /// The reconstruction range header was malformed.
199    #[error("range header must use bytes=<start>-<end> syntax")]
200    InvalidRangeHeader,
201    /// The reconstruction range start exceeded the end of the file.
202    #[error("requested range is not satisfiable")]
203    RangeNotSatisfiable,
204    /// The request did not include an authorization header.
205    #[error("authorization header is missing")]
206    MissingAuthorization,
207    /// The authorization header was malformed.
208    #[error("authorization header must use bearer format")]
209    InvalidAuthorizationHeader,
210    /// The bearer token was invalid.
211    #[error("bearer token was invalid")]
212    InvalidToken(TokenCodecError),
213    /// The bearer token did not grant the required scope.
214    #[error("bearer token does not grant the required scope")]
215    InsufficientScope,
216    /// The provider issuance endpoint is not configured.
217    #[error("provider token issuance endpoint is not configured")]
218    ProviderTokensDisabled,
219    /// The provider bootstrap key was missing.
220    #[error("provider bootstrap key is missing")]
221    MissingProviderApiKey,
222    /// The provider bootstrap key was invalid.
223    #[error("provider bootstrap key is invalid")]
224    InvalidProviderApiKey,
225    /// The provider subject was missing from a bootstrap request.
226    #[error("provider subject is missing")]
227    MissingProviderSubject,
228    /// The provider token issuance request contained invalid bounded metadata.
229    #[error("provider token request was invalid")]
230    InvalidProviderTokenRequest,
231    /// The provider webhook authentication header was missing.
232    #[error("provider webhook authentication is missing")]
233    MissingProviderWebhookAuthentication,
234    /// The provider webhook authentication header was invalid.
235    #[error("provider webhook authentication is invalid")]
236    InvalidProviderWebhookAuthentication,
237    /// The provider webhook payload was invalid.
238    #[error("provider webhook payload was invalid")]
239    InvalidProviderWebhookPayload,
240    /// The requested provider is not configured.
241    #[error("provider is not configured")]
242    UnknownProvider,
243    /// Provider authorization denied the request.
244    #[error("provider denied requested repository access")]
245    ProviderDenied,
246    /// Provider token issuance failed due to configuration or adapter errors.
247    #[error("provider token issuance failed")]
248    Provider(#[from] ProviderServiceError),
249    /// Reconstruction cache adapter access failed.
250    #[error("reconstruction cache adapter operation failed")]
251    ReconstructionCache(#[from] ReconstructionCacheError),
252    /// Server configuration was invalid for the selected runtime surface.
253    #[error("server configuration was invalid")]
254    Config(#[from] ServerConfigError),
255    /// The uploaded body did not match the expected SHA-256 identifier.
256    #[error("uploaded body hash did not match the expected sha256")]
257    ExpectedBodyHashMismatch,
258    /// A digest string was malformed.
259    #[error("digest must use sha256:<64 lowercase hex> format")]
260    InvalidDigest,
261    /// A repository name or namespace path was malformed.
262    #[error("repository name was invalid")]
263    InvalidRepositoryName,
264    /// A manifest reference or tag was malformed.
265    #[error("manifest reference was invalid")]
266    InvalidManifestReference,
267    /// The requested representation does not match any accepted media type.
268    #[error("requested representation was not acceptable")]
269    NotAcceptable,
270    /// The request requires a protocol-specific authentication challenge.
271    #[error("authorization challenge required")]
272    UnauthorizedChallenge(String),
273    /// An upload session identifier was malformed.
274    #[error("upload session identifier was invalid")]
275    InvalidUploadSession,
276    /// Too many OCI upload sessions are currently active.
277    #[error("too many active oci upload sessions")]
278    TooManyUploadSessions,
279    /// Too many OCI registry token exchanges are currently active.
280    #[error("too many active oci registry token requests")]
281    TooManyRegistryTokenRequests,
282    /// Redis reconstruction cache was selected without a URL.
283    #[error("redis reconstruction cache requires a redis url")]
284    MissingReconstructionCacheRedisUrl,
285    /// The transfer concurrency limiter was unexpectedly unavailable.
286    #[error("transfer concurrency limiter is unavailable")]
287    TransferLimiterClosed,
288    /// A blocking worker task failed before it could finish storage work.
289    #[error("blocking worker task failed")]
290    BlockingTask(#[source] JoinError),
291}
292
293impl ServerError {
294    const fn status_code(&self) -> StatusCode {
295        match self {
296            Self::InvalidFileId
297            | Self::InvalidContentHash
298            | Self::InvalidDigest
299            | Self::InvalidRepositoryName
300            | Self::InvalidManifestReference
301            | Self::InvalidUploadSession
302            | Self::InvalidXorbPrefix
303            | Self::HashParse(_) => StatusCode::BAD_REQUEST,
304            Self::NotAcceptable => StatusCode::NOT_ACCEPTABLE,
305            Self::UnauthorizedChallenge(_) => StatusCode::UNAUTHORIZED,
306            Self::InvalidRangeHeader => StatusCode::BAD_REQUEST,
307            Self::RangeNotSatisfiable => StatusCode::RANGE_NOT_SATISFIABLE,
308            Self::XorbHashMismatch
309            | Self::InvalidSerializedXorb
310            | Self::InvalidSerializedShard(_)
311            | Self::MissingReferencedXorb => StatusCode::BAD_REQUEST,
312            Self::TooManyShardTerms
313            | Self::TooManyBatchReconstructionFileIds
314            | Self::RequestBodyTooLarge => StatusCode::PAYLOAD_TOO_LARGE,
315            Self::RequestQueryTooLarge => StatusCode::URI_TOO_LONG,
316            Self::RequestBodyRead(_) | Self::RequestBodyFrameOutOfBounds => StatusCode::BAD_REQUEST,
317            Self::ExpectedBodyHashMismatch => StatusCode::BAD_REQUEST,
318            Self::NotFound | Self::UnknownProvider | Self::ProviderTokensDisabled => {
319                StatusCode::NOT_FOUND
320            }
321            Self::MissingAuthorization
322            | Self::InvalidAuthorizationHeader
323            | Self::InvalidToken(_)
324            | Self::MissingProviderApiKey
325            | Self::MissingProviderSubject
326            | Self::MissingProviderWebhookAuthentication => StatusCode::UNAUTHORIZED,
327            Self::InsufficientScope
328            | Self::InvalidProviderApiKey
329            | Self::InvalidProviderWebhookAuthentication
330            | Self::ProviderDenied => StatusCode::FORBIDDEN,
331            Self::InvalidProviderTokenRequest | Self::InvalidProviderWebhookPayload => {
332                StatusCode::BAD_REQUEST
333            }
334            Self::TooManyUploadSessions | Self::TooManyRegistryTokenRequests => {
335                StatusCode::TOO_MANY_REQUESTS
336            }
337            Self::TransferLimiterClosed => StatusCode::SERVICE_UNAVAILABLE,
338            Self::Io(_)
339            | Self::Json(_)
340            | Self::NumericConversion(_)
341            | Self::ObjectStore(_)
342            | Self::Index(_)
343            | Self::StoredFileMetadataTooLarge { .. }
344            | Self::StoredFileMetadataLengthMismatch
345            | Self::Config(_)
346            | Self::Overflow
347            | Self::MissingReconstructionCacheRedisUrl
348            | Self::ReconstructionCache(_)
349            | Self::BlockingTask(_)
350            | Self::Provider(_) => StatusCode::INTERNAL_SERVER_ERROR,
351        }
352    }
353}
354
355impl IntoResponse for ServerError {
356    fn into_response(self) -> Response {
357        let status = self.status_code();
358        let should_attach_default_challenge = matches!(
359            self,
360            Self::MissingAuthorization | Self::InvalidAuthorizationHeader | Self::InvalidToken(_)
361        );
362        let custom_challenge = if let Self::UnauthorizedChallenge(custom_header) = &self {
363            Some(custom_header.as_str())
364        } else {
365            None
366        };
367        let body = ErrorBody {
368            error: self.to_string(),
369        };
370        let mut response = (status, Json(body)).into_response();
371        if let Some(custom_header) = custom_challenge {
372            if let Ok(header_value) = HeaderValue::from_str(custom_header) {
373                response
374                    .headers_mut()
375                    .insert(WWW_AUTHENTICATE, header_value);
376            }
377        } else if should_attach_default_challenge {
378            response.headers_mut().insert(
379                WWW_AUTHENTICATE,
380                HeaderValue::from_static("Bearer realm=\"shardline\""),
381            );
382        }
383        response
384    }
385}
386
387impl From<HttpRangeParseError> for ServerError {
388    fn from(value: HttpRangeParseError) -> Self {
389        match value {
390            HttpRangeParseError::Unsatisfiable => Self::RangeNotSatisfiable,
391            HttpRangeParseError::MissingBytesUnit
392            | HttpRangeParseError::InvalidSyntax
393            | HttpRangeParseError::InvalidNumber => Self::InvalidRangeHeader,
394        }
395    }
396}
397
398impl From<XorbParseError> for ServerError {
399    fn from(value: XorbParseError) -> Self {
400        match value {
401            XorbParseError::HashMismatch => Self::XorbHashMismatch,
402            XorbParseError::InvalidFormat(_)
403            | XorbParseError::NumericConversion(_)
404            | XorbParseError::Io(_) => Self::InvalidSerializedXorb,
405        }
406    }
407}
408
409impl From<XetAdapterError> for ServerError {
410    fn from(value: XetAdapterError) -> Self {
411        match value {
412            XetAdapterError::Io(e) => Self::Io(e),
413            XetAdapterError::NumericConversion(e) => Self::NumericConversion(e),
414            XetAdapterError::HashParse(e) => Self::HashParse(e),
415            XetAdapterError::ObjectStore(e) => Self::from(e),
416            XetAdapterError::LocalObjectStore(e) => Self::ObjectStore(ObjectStoreError::Local(e)),
417            XetAdapterError::S3ObjectStore(e) => Self::ObjectStore(ObjectStoreError::S3(e)),
418            XetAdapterError::IndexStore(e) => Self::Index(IndexError::Local(e)),
419            XetAdapterError::MemoryIndexStore(e) => Self::Index(IndexError::MemoryIndex(e)),
420            XetAdapterError::MemoryRecordStore(e) => Self::Index(IndexError::MemoryRecord(e)),
421            XetAdapterError::PostgresMetadata(e) => Self::Index(IndexError::PostgresMetadata(e)),
422            XetAdapterError::FileRecordInvariant(e) => {
423                Self::Index(IndexError::FileRecordInvariant(e))
424            }
425            XetAdapterError::InvalidContentHash => Self::InvalidContentHash,
426            XetAdapterError::InvalidXorbPrefix => Self::InvalidXorbPrefix,
427            XetAdapterError::XorbHashMismatch => Self::XorbHashMismatch,
428            XetAdapterError::InvalidSerializedXorb => Self::InvalidSerializedXorb,
429            XetAdapterError::InvalidSerializedShard(e) => Self::InvalidSerializedShard(e),
430            XetAdapterError::MissingReferencedXorb => Self::MissingReferencedXorb,
431            XetAdapterError::TooManyShardTerms => Self::TooManyShardTerms,
432            XetAdapterError::NotFound => Self::NotFound,
433            XetAdapterError::Overflow => Self::Overflow,
434            XetAdapterError::RangeNotSatisfiable => Self::RangeNotSatisfiable,
435        }
436    }
437}
438
439impl From<ProviderEventsError> for ServerError {
440    fn from(value: ProviderEventsError) -> Self {
441        match value {
442            ProviderEventsError::Overflow => Self::Overflow,
443            ProviderEventsError::InvalidContentHash => Self::InvalidContentHash,
444            ProviderEventsError::InvalidProviderWebhookPayload => {
445                Self::InvalidProviderWebhookPayload
446            }
447            ProviderEventsError::ConflictingRenameTargetRecord => {
448                Self::Index(IndexError::ConflictingRenameTargetRecord)
449            }
450            ProviderEventsError::Json(e) => Self::Json(e),
451            ProviderEventsError::NumericConversion(e) => Self::NumericConversion(e),
452            ProviderEventsError::RetentionHold(e) => Self::Index(IndexError::RetentionHold(e)),
453            ProviderEventsError::XetAdapter(e) => Self::from(e),
454            ProviderEventsError::IndexStore(e) => Self::Index(IndexError::Local(e)),
455            ProviderEventsError::MemoryIndexStore(e) => Self::Index(IndexError::MemoryIndex(e)),
456            ProviderEventsError::MemoryRecordStore(e) => Self::Index(IndexError::MemoryRecord(e)),
457            ProviderEventsError::PostgresMetadata(e) => {
458                Self::Index(IndexError::PostgresMetadata(e))
459            }
460            ProviderEventsError::WebhookDelivery(e) => Self::Index(IndexError::WebhookDelivery(e)),
461            ProviderEventsError::ObjectStore(e) => Self::from(e),
462            ProviderEventsError::ParseStoredFileRecord(e) => Self::Io(std::io::Error::new(
463                std::io::ErrorKind::InvalidData,
464                e.to_string(),
465            )),
466        }
467    }
468}
469
470impl From<GcError> for ServerError {
471    fn from(err: GcError) -> Self {
472        match err {
473            GcError::Io(e) => Self::Io(e),
474            GcError::Json(e) => Self::Json(e),
475            GcError::NumericConversion(e) => Self::NumericConversion(e),
476            GcError::ObjectStore(e) => Self::from(e),
477            GcError::LocalObjectStore(e) => Self::ObjectStore(ObjectStoreError::Local(e)),
478            GcError::S3ObjectStore(e) => Self::ObjectStore(ObjectStoreError::S3(e)),
479            GcError::ObjectPrefix(e) => Self::ObjectStore(ObjectStoreError::Prefix(e)),
480            GcError::IndexStore(e) => Self::Index(IndexError::Local(e)),
481            GcError::MemoryIndexStore(e) => Self::Index(IndexError::MemoryIndex(e)),
482            GcError::MemoryRecordStore(e) => Self::Index(IndexError::MemoryRecord(e)),
483            GcError::PostgresMetadata(e) => Self::Index(IndexError::PostgresMetadata(e)),
484            GcError::RetentionHold(e) => Self::Index(IndexError::RetentionHold(e)),
485            GcError::QuarantineCandidate(e) => Self::Index(IndexError::QuarantineCandidate(e)),
486            GcError::WebhookDelivery(e) => Self::Index(IndexError::WebhookDelivery(e)),
487            GcError::FileRecordInvariant(e) => Self::Index(IndexError::FileRecordInvariant(e)),
488            GcError::InvalidLifecycleMetadata(e) => {
489                Self::Index(IndexError::InvalidLifecycleMetadata(e))
490            }
491            GcError::InvalidContentHash => Self::InvalidContentHash,
492            GcError::Overflow => Self::Overflow,
493            GcError::XetAdapter(e) => Self::from(e),
494        }
495    }
496}
497
498impl From<OciAdapterError> for ServerError {
499    fn from(value: OciAdapterError) -> Self {
500        match value {
501            OciAdapterError::Io(e) => Self::Io(e),
502            OciAdapterError::Json(e) => Self::Json(e),
503            OciAdapterError::NumericConversion(e) => Self::NumericConversion(e),
504            OciAdapterError::ObjectStore(e) => Self::from(e),
505            OciAdapterError::S3ObjectStore(e) => Self::ObjectStore(ObjectStoreError::S3(e)),
506            OciAdapterError::LocalObjectStore(e) => Self::ObjectStore(ObjectStoreError::Local(e)),
507            OciAdapterError::ObjectPrefix(e) => Self::ObjectStore(ObjectStoreError::Prefix(e)),
508            OciAdapterError::NotFound => Self::NotFound,
509            OciAdapterError::Overflow => Self::Overflow,
510            OciAdapterError::InvalidContentHash => Self::InvalidContentHash,
511            OciAdapterError::InvalidDigest => Self::InvalidDigest,
512            OciAdapterError::InvalidRepositoryName => Self::InvalidRepositoryName,
513            OciAdapterError::InvalidManifestReference => Self::InvalidManifestReference,
514            OciAdapterError::InvalidUploadSession => Self::InvalidUploadSession,
515            OciAdapterError::TooManyUploadSessions => Self::TooManyUploadSessions,
516            OciAdapterError::ExpectedBodyHashMismatch => Self::ExpectedBodyHashMismatch,
517            OciAdapterError::BlockingTask(e) => Self::BlockingTask(e),
518        }
519    }
520}
521
522impl From<shardline_protocol_adapters::ProtocolError> for ServerError {
523    fn from(error: shardline_protocol_adapters::ProtocolError) -> Self {
524        match error {
525            shardline_protocol_adapters::ProtocolError::InvalidContentHash => {
526                Self::InvalidContentHash
527            }
528        }
529    }
530}
531
532impl From<LocalObjectStoreError> for ServerError {
533    fn from(e: LocalObjectStoreError) -> Self {
534        Self::ObjectStore(ObjectStoreError::Local(e))
535    }
536}
537
538impl From<S3ObjectStoreError> for ServerError {
539    fn from(e: S3ObjectStoreError) -> Self {
540        Self::ObjectStore(ObjectStoreError::S3(e))
541    }
542}
543
544impl From<ObjectPrefixError> for ServerError {
545    fn from(e: ObjectPrefixError) -> Self {
546        Self::ObjectStore(ObjectStoreError::Prefix(e))
547    }
548}
549
550impl From<LocalIndexStoreError> for ServerError {
551    fn from(e: LocalIndexStoreError) -> Self {
552        Self::Index(IndexError::Local(e))
553    }
554}
555
556impl From<MemoryIndexStoreError> for ServerError {
557    fn from(e: MemoryIndexStoreError) -> Self {
558        Self::Index(IndexError::MemoryIndex(e))
559    }
560}
561
562impl From<MemoryRecordStoreError> for ServerError {
563    fn from(e: MemoryRecordStoreError) -> Self {
564        Self::Index(IndexError::MemoryRecord(e))
565    }
566}
567
568impl From<PostgresMetadataStoreError> for ServerError {
569    fn from(e: PostgresMetadataStoreError) -> Self {
570        Self::Index(IndexError::PostgresMetadata(e))
571    }
572}
573
574impl From<RetentionHoldError> for ServerError {
575    fn from(e: RetentionHoldError) -> Self {
576        Self::Index(IndexError::RetentionHold(e))
577    }
578}
579
580impl From<QuarantineCandidateError> for ServerError {
581    fn from(e: QuarantineCandidateError) -> Self {
582        Self::Index(IndexError::QuarantineCandidate(e))
583    }
584}
585
586impl From<WebhookDeliveryError> for ServerError {
587    fn from(e: WebhookDeliveryError) -> Self {
588        Self::Index(IndexError::WebhookDelivery(e))
589    }
590}
591
592impl From<FileRecordInvariantError> for ServerError {
593    fn from(e: FileRecordInvariantError) -> Self {
594        Self::Index(IndexError::FileRecordInvariant(e))
595    }
596}
597
598impl From<InvalidLifecycleMetadataError> for ServerError {
599    fn from(e: InvalidLifecycleMetadataError) -> Self {
600        Self::Index(IndexError::InvalidLifecycleMetadata(e))
601    }
602}
603
604impl From<InvalidReconstructionResponseError> for ServerError {
605    fn from(e: InvalidReconstructionResponseError) -> Self {
606        Self::Index(IndexError::InvalidReconstructionResponse(e))
607    }
608}
609
610impl From<ParseStoredFileRecordError> for ServerError {
611    fn from(e: ParseStoredFileRecordError) -> Self {
612        match e {
613            ParseStoredFileRecordError::StoredFileMetadataTooLarge {
614                observed_bytes,
615                maximum_bytes,
616            } => Self::StoredFileMetadataTooLarge {
617                observed_bytes,
618                maximum_bytes,
619            },
620            ParseStoredFileRecordError::Json(e) => Self::Json(e),
621        }
622    }
623}
624
625#[derive(Debug, Serialize)]
626struct ErrorBody {
627    error: String,
628}
629
630#[cfg(test)]
631mod tests {
632    use axum::http::StatusCode;
633
634    use super::ServerError;
635
636    fn status_for(error: &ServerError) -> StatusCode {
637        error.status_code()
638    }
639
640    #[test]
641    fn not_found_maps_to_404() {
642        assert_eq!(status_for(&ServerError::NotFound), StatusCode::NOT_FOUND);
643    }
644
645    #[test]
646    fn insufficient_scope_maps_to_403() {
647        assert_eq!(
648            status_for(&ServerError::InsufficientScope),
649            StatusCode::FORBIDDEN
650        );
651    }
652
653    #[test]
654    fn too_many_upload_sessions_maps_to_429() {
655        assert_eq!(
656            status_for(&ServerError::TooManyUploadSessions),
657            StatusCode::TOO_MANY_REQUESTS
658        );
659    }
660
661    #[test]
662    fn too_many_registry_token_requests_maps_to_429() {
663        assert_eq!(
664            status_for(&ServerError::TooManyRegistryTokenRequests),
665            StatusCode::TOO_MANY_REQUESTS
666        );
667    }
668
669    #[test]
670    fn missing_authorization_maps_to_401() {
671        assert_eq!(
672            status_for(&ServerError::MissingAuthorization),
673            StatusCode::UNAUTHORIZED
674        );
675    }
676
677    #[test]
678    fn request_body_too_large_maps_to_413() {
679        assert_eq!(
680            status_for(&ServerError::RequestBodyTooLarge),
681            StatusCode::PAYLOAD_TOO_LARGE
682        );
683    }
684
685    #[test]
686    fn invalid_file_id_maps_to_400() {
687        assert_eq!(
688            status_for(&ServerError::InvalidFileId),
689            StatusCode::BAD_REQUEST
690        );
691    }
692
693    #[test]
694    fn range_not_satisfiable_maps_to_416() {
695        assert_eq!(
696            status_for(&ServerError::RangeNotSatisfiable),
697            StatusCode::RANGE_NOT_SATISFIABLE
698        );
699    }
700
701    #[test]
702    fn not_acceptable_maps_to_406() {
703        assert_eq!(
704            status_for(&ServerError::NotAcceptable),
705            StatusCode::NOT_ACCEPTABLE
706        );
707    }
708
709    #[test]
710    fn io_error_maps_to_500() {
711        let io_err = std::io::Error::other("test");
712        assert_eq!(
713            status_for(&ServerError::Io(io_err)),
714            StatusCode::INTERNAL_SERVER_ERROR
715        );
716    }
717
718    #[test]
719    fn transfer_limiter_closed_maps_to_503() {
720        assert_eq!(
721            status_for(&ServerError::TransferLimiterClosed),
722            StatusCode::SERVICE_UNAVAILABLE
723        );
724    }
725
726    #[test]
727    fn provider_denied_maps_to_403() {
728        assert_eq!(
729            status_for(&ServerError::ProviderDenied),
730            StatusCode::FORBIDDEN
731        );
732    }
733
734    #[test]
735    fn unknown_provider_maps_to_404() {
736        assert_eq!(
737            status_for(&ServerError::UnknownProvider),
738            StatusCode::NOT_FOUND
739        );
740    }
741
742    #[test]
743    fn overflow_maps_to_500() {
744        assert_eq!(
745            status_for(&ServerError::Overflow),
746            StatusCode::INTERNAL_SERVER_ERROR
747        );
748    }
749
750    #[test]
751    fn invalid_content_hash_maps_to_400() {
752        assert_eq!(
753            status_for(&ServerError::InvalidContentHash),
754            StatusCode::BAD_REQUEST
755        );
756    }
757
758    #[test]
759    fn expected_body_hash_mismatch_maps_to_400() {
760        assert_eq!(
761            status_for(&ServerError::ExpectedBodyHashMismatch),
762            StatusCode::BAD_REQUEST
763        );
764    }
765}