use std::{io::Error as IoError, num::TryFromIntError};
use axum::{
Error as AxumError, Json,
http::{HeaderValue, StatusCode, header::WWW_AUTHENTICATE},
response::{IntoResponse, Response},
};
use serde::Serialize;
use serde_json::Error as JsonError;
use shardline_cache::ReconstructionCacheError;
use shardline_index::{
FileRecordInvariantError, LocalIndexStoreError, MemoryIndexStoreError, MemoryRecordStoreError,
PostgresMetadataStoreError, QuarantineCandidateError, RetentionHoldError, WebhookDeliveryError,
};
use shardline_protocol::{HashParseError, HttpRangeParseError, TokenCodecError};
pub use shardline_server_core::{
InvalidLifecycleMetadataError, InvalidReconstructionResponseError, InvalidSerializedShardError,
ParseStoredFileRecordError,
};
use shardline_storage::{LocalObjectStoreError, ObjectPrefixError, S3ObjectStoreError};
use thiserror::Error;
use tokio::task::JoinError;
use crate::{
config::ServerConfigError, provider::ProviderServiceError, xet_adapter::XorbParseError,
};
use shardline_gc::GcError;
use shardline_oci_adapter::OciAdapterError;
use shardline_provider_events::ProviderEventsError;
use shardline_xet_adapter::XetAdapterError;
#[derive(Debug, Error)]
pub enum ObjectStoreError {
#[error("local storage operation failed")]
Local(#[from] LocalObjectStoreError),
#[error("s3 object storage adapter operation failed")]
S3(#[from] S3ObjectStoreError),
#[error("object storage prefix validation failed")]
Prefix(#[from] ObjectPrefixError),
#[error("s3 object storage configuration is missing")]
MissingS3Config,
#[error("stored object length did not match indexed metadata")]
StoredLengthMismatch,
#[error(
"storage migration source object hash mismatch for key {key}: expected {expected_hash}, observed {observed_hash}"
)]
MigrationSourceHashMismatch {
key: String,
expected_hash: String,
observed_hash: String,
},
}
#[derive(Debug, Error)]
pub enum IndexError {
#[error("index adapter operation failed")]
Local(#[from] LocalIndexStoreError),
#[error("memory index adapter operation failed")]
MemoryIndex(#[from] MemoryIndexStoreError),
#[error("memory record adapter operation failed")]
MemoryRecord(#[from] MemoryRecordStoreError),
#[error("postgres metadata adapter operation failed")]
PostgresMetadata(#[from] PostgresMetadataStoreError),
#[error("retention hold input was invalid")]
RetentionHold(#[from] RetentionHoldError),
#[error("quarantine candidate input was invalid")]
QuarantineCandidate(#[from] QuarantineCandidateError),
#[error("webhook delivery metadata was invalid")]
WebhookDelivery(#[from] WebhookDeliveryError),
#[error("stored file metadata was invalid")]
FileRecordInvariant(#[from] FileRecordInvariantError),
#[error("lifecycle metadata was internally inconsistent")]
InvalidLifecycleMetadata(#[from] InvalidLifecycleMetadataError),
#[error("reconstruction response invariant failed: {0}")]
InvalidReconstructionResponse(#[from] InvalidReconstructionResponseError),
#[error("required metadata table is missing: {0}")]
MissingRequiredMetadataTable(String),
#[error("repository rename target already contains conflicting metadata")]
ConflictingRenameTargetRecord,
}
#[derive(Debug, Error)]
pub enum ServerError {
#[error("local storage operation failed")]
Io(#[from] IoError),
#[error("json operation failed")]
Json(#[from] JsonError),
#[error("request body stream failed")]
RequestBodyRead(#[source] AxumError),
#[error("request body exceeded the configured maximum accepted byte count")]
RequestBodyTooLarge,
#[error("request query exceeded the bounded metadata parser budget")]
RequestQueryTooLarge,
#[error("request body frame exceeded checked bounds")]
RequestBodyFrameOutOfBounds,
#[error("numeric conversion exceeded supported bounds")]
NumericConversion(#[from] TryFromIntError),
#[error("invalid content hash")]
HashParse(#[from] HashParseError),
#[error("object storage operation failed")]
ObjectStore(#[from] ObjectStoreError),
#[error("index adapter operation failed")]
Index(#[from] IndexError),
#[error("stored file metadata exceeded the bounded parser ceiling")]
StoredFileMetadataTooLarge {
observed_bytes: u64,
maximum_bytes: u64,
},
#[error("stored file metadata length did not match the validated length")]
StoredFileMetadataLengthMismatch,
#[error(
"file identifier must be relative and must not contain traversal or control characters"
)]
InvalidFileId,
#[error("content hash must be 64 hexadecimal characters")]
InvalidContentHash,
#[error("xorb transfer prefix must be default")]
InvalidXorbPrefix,
#[error("xorb body hash did not match the requested path hash")]
XorbHashMismatch,
#[error("xorb body was not a valid serialized xorb object")]
InvalidSerializedXorb,
#[error("shard body was not a valid serialized shard object")]
InvalidSerializedShard(#[from] InvalidSerializedShardError),
#[error("shard referenced a missing xorb")]
MissingReferencedXorb,
#[error("shard metadata exceeded bounded parser safety limits")]
TooManyShardTerms,
#[error("batch reconstruction requested too many file identifiers")]
TooManyBatchReconstructionFileIds,
#[error("content not found")]
NotFound,
#[error("arithmetic overflow")]
Overflow,
#[error("range header must use bytes=<start>-<end> syntax")]
InvalidRangeHeader,
#[error("requested range is not satisfiable")]
RangeNotSatisfiable,
#[error("authorization header is missing")]
MissingAuthorization,
#[error("authorization header must use bearer format")]
InvalidAuthorizationHeader,
#[error("bearer token was invalid")]
InvalidToken(TokenCodecError),
#[error("bearer token does not grant the required scope")]
InsufficientScope,
#[error("provider token issuance endpoint is not configured")]
ProviderTokensDisabled,
#[error("provider bootstrap key is missing")]
MissingProviderApiKey,
#[error("provider bootstrap key is invalid")]
InvalidProviderApiKey,
#[error("provider subject is missing")]
MissingProviderSubject,
#[error("provider token request was invalid")]
InvalidProviderTokenRequest,
#[error("provider webhook authentication is missing")]
MissingProviderWebhookAuthentication,
#[error("provider webhook authentication is invalid")]
InvalidProviderWebhookAuthentication,
#[error("provider webhook payload was invalid")]
InvalidProviderWebhookPayload,
#[error("provider is not configured")]
UnknownProvider,
#[error("provider denied requested repository access")]
ProviderDenied,
#[error("provider token issuance failed")]
Provider(#[from] ProviderServiceError),
#[error("reconstruction cache adapter operation failed")]
ReconstructionCache(#[from] ReconstructionCacheError),
#[error("server configuration was invalid")]
Config(#[from] ServerConfigError),
#[error("uploaded body hash did not match the expected sha256")]
ExpectedBodyHashMismatch,
#[error("digest must use sha256:<64 lowercase hex> format")]
InvalidDigest,
#[error("repository name was invalid")]
InvalidRepositoryName,
#[error("manifest reference was invalid")]
InvalidManifestReference,
#[error("requested representation was not acceptable")]
NotAcceptable,
#[error("authorization challenge required")]
UnauthorizedChallenge(String),
#[error("upload session identifier was invalid")]
InvalidUploadSession,
#[error("too many active oci upload sessions")]
TooManyUploadSessions,
#[error("too many active oci registry token requests")]
TooManyRegistryTokenRequests,
#[error("redis reconstruction cache requires a redis url")]
MissingReconstructionCacheRedisUrl,
#[error("transfer concurrency limiter is unavailable")]
TransferLimiterClosed,
#[error("blocking worker task failed")]
BlockingTask(#[source] JoinError),
}
impl ServerError {
const fn status_code(&self) -> StatusCode {
match self {
Self::InvalidFileId
| Self::InvalidContentHash
| Self::InvalidDigest
| Self::InvalidRepositoryName
| Self::InvalidManifestReference
| Self::InvalidUploadSession
| Self::InvalidXorbPrefix
| Self::HashParse(_) => StatusCode::BAD_REQUEST,
Self::NotAcceptable => StatusCode::NOT_ACCEPTABLE,
Self::UnauthorizedChallenge(_) => StatusCode::UNAUTHORIZED,
Self::InvalidRangeHeader => StatusCode::BAD_REQUEST,
Self::RangeNotSatisfiable => StatusCode::RANGE_NOT_SATISFIABLE,
Self::XorbHashMismatch
| Self::InvalidSerializedXorb
| Self::InvalidSerializedShard(_)
| Self::MissingReferencedXorb => StatusCode::BAD_REQUEST,
Self::TooManyShardTerms
| Self::TooManyBatchReconstructionFileIds
| Self::RequestBodyTooLarge => StatusCode::PAYLOAD_TOO_LARGE,
Self::RequestQueryTooLarge => StatusCode::URI_TOO_LONG,
Self::RequestBodyRead(_) | Self::RequestBodyFrameOutOfBounds => StatusCode::BAD_REQUEST,
Self::ExpectedBodyHashMismatch => StatusCode::BAD_REQUEST,
Self::NotFound | Self::UnknownProvider | Self::ProviderTokensDisabled => {
StatusCode::NOT_FOUND
}
Self::MissingAuthorization
| Self::InvalidAuthorizationHeader
| Self::InvalidToken(_)
| Self::MissingProviderApiKey
| Self::MissingProviderSubject
| Self::MissingProviderWebhookAuthentication => StatusCode::UNAUTHORIZED,
Self::InsufficientScope
| Self::InvalidProviderApiKey
| Self::InvalidProviderWebhookAuthentication
| Self::ProviderDenied => StatusCode::FORBIDDEN,
Self::InvalidProviderTokenRequest | Self::InvalidProviderWebhookPayload => {
StatusCode::BAD_REQUEST
}
Self::TooManyUploadSessions | Self::TooManyRegistryTokenRequests => {
StatusCode::TOO_MANY_REQUESTS
}
Self::TransferLimiterClosed => StatusCode::SERVICE_UNAVAILABLE,
Self::Io(_)
| Self::Json(_)
| Self::NumericConversion(_)
| Self::ObjectStore(_)
| Self::Index(_)
| Self::StoredFileMetadataTooLarge { .. }
| Self::StoredFileMetadataLengthMismatch
| Self::Config(_)
| Self::Overflow
| Self::MissingReconstructionCacheRedisUrl
| Self::ReconstructionCache(_)
| Self::BlockingTask(_)
| Self::Provider(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
impl IntoResponse for ServerError {
fn into_response(self) -> Response {
let status = self.status_code();
let should_attach_default_challenge = matches!(
self,
Self::MissingAuthorization | Self::InvalidAuthorizationHeader | Self::InvalidToken(_)
);
let custom_challenge = if let Self::UnauthorizedChallenge(custom_header) = &self {
Some(custom_header.as_str())
} else {
None
};
let body = ErrorBody {
error: self.to_string(),
};
let mut response = (status, Json(body)).into_response();
if let Some(custom_header) = custom_challenge {
if let Ok(header_value) = HeaderValue::from_str(custom_header) {
response
.headers_mut()
.insert(WWW_AUTHENTICATE, header_value);
}
} else if should_attach_default_challenge {
response.headers_mut().insert(
WWW_AUTHENTICATE,
HeaderValue::from_static("Bearer realm=\"shardline\""),
);
}
response
}
}
impl From<HttpRangeParseError> for ServerError {
fn from(value: HttpRangeParseError) -> Self {
match value {
HttpRangeParseError::Unsatisfiable => Self::RangeNotSatisfiable,
HttpRangeParseError::MissingBytesUnit
| HttpRangeParseError::InvalidSyntax
| HttpRangeParseError::InvalidNumber => Self::InvalidRangeHeader,
}
}
}
impl From<XorbParseError> for ServerError {
fn from(value: XorbParseError) -> Self {
match value {
XorbParseError::HashMismatch => Self::XorbHashMismatch,
XorbParseError::InvalidFormat(_)
| XorbParseError::NumericConversion(_)
| XorbParseError::Io(_) => Self::InvalidSerializedXorb,
}
}
}
impl From<XetAdapterError> for ServerError {
fn from(value: XetAdapterError) -> Self {
match value {
XetAdapterError::Io(e) => Self::Io(e),
XetAdapterError::NumericConversion(e) => Self::NumericConversion(e),
XetAdapterError::HashParse(e) => Self::HashParse(e),
XetAdapterError::ObjectStore(e) => Self::from(e),
XetAdapterError::LocalObjectStore(e) => Self::ObjectStore(ObjectStoreError::Local(e)),
XetAdapterError::S3ObjectStore(e) => Self::ObjectStore(ObjectStoreError::S3(e)),
XetAdapterError::IndexStore(e) => Self::Index(IndexError::Local(e)),
XetAdapterError::MemoryIndexStore(e) => Self::Index(IndexError::MemoryIndex(e)),
XetAdapterError::MemoryRecordStore(e) => Self::Index(IndexError::MemoryRecord(e)),
XetAdapterError::PostgresMetadata(e) => Self::Index(IndexError::PostgresMetadata(e)),
XetAdapterError::FileRecordInvariant(e) => {
Self::Index(IndexError::FileRecordInvariant(e))
}
XetAdapterError::InvalidContentHash => Self::InvalidContentHash,
XetAdapterError::InvalidXorbPrefix => Self::InvalidXorbPrefix,
XetAdapterError::XorbHashMismatch => Self::XorbHashMismatch,
XetAdapterError::InvalidSerializedXorb => Self::InvalidSerializedXorb,
XetAdapterError::InvalidSerializedShard(e) => Self::InvalidSerializedShard(e),
XetAdapterError::MissingReferencedXorb => Self::MissingReferencedXorb,
XetAdapterError::TooManyShardTerms => Self::TooManyShardTerms,
XetAdapterError::NotFound => Self::NotFound,
XetAdapterError::Overflow => Self::Overflow,
XetAdapterError::RangeNotSatisfiable => Self::RangeNotSatisfiable,
}
}
}
impl From<ProviderEventsError> for ServerError {
fn from(value: ProviderEventsError) -> Self {
match value {
ProviderEventsError::Overflow => Self::Overflow,
ProviderEventsError::InvalidContentHash => Self::InvalidContentHash,
ProviderEventsError::InvalidProviderWebhookPayload => {
Self::InvalidProviderWebhookPayload
}
ProviderEventsError::ConflictingRenameTargetRecord => {
Self::Index(IndexError::ConflictingRenameTargetRecord)
}
ProviderEventsError::Json(e) => Self::Json(e),
ProviderEventsError::NumericConversion(e) => Self::NumericConversion(e),
ProviderEventsError::RetentionHold(e) => Self::Index(IndexError::RetentionHold(e)),
ProviderEventsError::XetAdapter(e) => Self::from(e),
ProviderEventsError::IndexStore(e) => Self::Index(IndexError::Local(e)),
ProviderEventsError::MemoryIndexStore(e) => Self::Index(IndexError::MemoryIndex(e)),
ProviderEventsError::MemoryRecordStore(e) => Self::Index(IndexError::MemoryRecord(e)),
ProviderEventsError::PostgresMetadata(e) => {
Self::Index(IndexError::PostgresMetadata(e))
}
ProviderEventsError::WebhookDelivery(e) => Self::Index(IndexError::WebhookDelivery(e)),
ProviderEventsError::ObjectStore(e) => Self::from(e),
ProviderEventsError::ParseStoredFileRecord(e) => Self::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
e.to_string(),
)),
}
}
}
impl From<GcError> for ServerError {
fn from(err: GcError) -> Self {
match err {
GcError::Io(e) => Self::Io(e),
GcError::Json(e) => Self::Json(e),
GcError::NumericConversion(e) => Self::NumericConversion(e),
GcError::ObjectStore(e) => Self::from(e),
GcError::LocalObjectStore(e) => Self::ObjectStore(ObjectStoreError::Local(e)),
GcError::S3ObjectStore(e) => Self::ObjectStore(ObjectStoreError::S3(e)),
GcError::ObjectPrefix(e) => Self::ObjectStore(ObjectStoreError::Prefix(e)),
GcError::IndexStore(e) => Self::Index(IndexError::Local(e)),
GcError::MemoryIndexStore(e) => Self::Index(IndexError::MemoryIndex(e)),
GcError::MemoryRecordStore(e) => Self::Index(IndexError::MemoryRecord(e)),
GcError::PostgresMetadata(e) => Self::Index(IndexError::PostgresMetadata(e)),
GcError::RetentionHold(e) => Self::Index(IndexError::RetentionHold(e)),
GcError::QuarantineCandidate(e) => Self::Index(IndexError::QuarantineCandidate(e)),
GcError::WebhookDelivery(e) => Self::Index(IndexError::WebhookDelivery(e)),
GcError::FileRecordInvariant(e) => Self::Index(IndexError::FileRecordInvariant(e)),
GcError::InvalidLifecycleMetadata(e) => {
Self::Index(IndexError::InvalidLifecycleMetadata(e))
}
GcError::InvalidContentHash => Self::InvalidContentHash,
GcError::Overflow => Self::Overflow,
GcError::XetAdapter(e) => Self::from(e),
}
}
}
impl From<OciAdapterError> for ServerError {
fn from(value: OciAdapterError) -> Self {
match value {
OciAdapterError::Io(e) => Self::Io(e),
OciAdapterError::Json(e) => Self::Json(e),
OciAdapterError::NumericConversion(e) => Self::NumericConversion(e),
OciAdapterError::ObjectStore(e) => Self::from(e),
OciAdapterError::S3ObjectStore(e) => Self::ObjectStore(ObjectStoreError::S3(e)),
OciAdapterError::LocalObjectStore(e) => Self::ObjectStore(ObjectStoreError::Local(e)),
OciAdapterError::ObjectPrefix(e) => Self::ObjectStore(ObjectStoreError::Prefix(e)),
OciAdapterError::NotFound => Self::NotFound,
OciAdapterError::Overflow => Self::Overflow,
OciAdapterError::InvalidContentHash => Self::InvalidContentHash,
OciAdapterError::InvalidDigest => Self::InvalidDigest,
OciAdapterError::InvalidRepositoryName => Self::InvalidRepositoryName,
OciAdapterError::InvalidManifestReference => Self::InvalidManifestReference,
OciAdapterError::InvalidUploadSession => Self::InvalidUploadSession,
OciAdapterError::TooManyUploadSessions => Self::TooManyUploadSessions,
OciAdapterError::ExpectedBodyHashMismatch => Self::ExpectedBodyHashMismatch,
OciAdapterError::BlockingTask(e) => Self::BlockingTask(e),
}
}
}
impl From<shardline_protocol_adapters::ProtocolError> for ServerError {
fn from(error: shardline_protocol_adapters::ProtocolError) -> Self {
match error {
shardline_protocol_adapters::ProtocolError::InvalidContentHash => {
Self::InvalidContentHash
}
}
}
}
impl From<LocalObjectStoreError> for ServerError {
fn from(e: LocalObjectStoreError) -> Self {
Self::ObjectStore(ObjectStoreError::Local(e))
}
}
impl From<S3ObjectStoreError> for ServerError {
fn from(e: S3ObjectStoreError) -> Self {
Self::ObjectStore(ObjectStoreError::S3(e))
}
}
impl From<ObjectPrefixError> for ServerError {
fn from(e: ObjectPrefixError) -> Self {
Self::ObjectStore(ObjectStoreError::Prefix(e))
}
}
impl From<LocalIndexStoreError> for ServerError {
fn from(e: LocalIndexStoreError) -> Self {
Self::Index(IndexError::Local(e))
}
}
impl From<MemoryIndexStoreError> for ServerError {
fn from(e: MemoryIndexStoreError) -> Self {
Self::Index(IndexError::MemoryIndex(e))
}
}
impl From<MemoryRecordStoreError> for ServerError {
fn from(e: MemoryRecordStoreError) -> Self {
Self::Index(IndexError::MemoryRecord(e))
}
}
impl From<PostgresMetadataStoreError> for ServerError {
fn from(e: PostgresMetadataStoreError) -> Self {
Self::Index(IndexError::PostgresMetadata(e))
}
}
impl From<RetentionHoldError> for ServerError {
fn from(e: RetentionHoldError) -> Self {
Self::Index(IndexError::RetentionHold(e))
}
}
impl From<QuarantineCandidateError> for ServerError {
fn from(e: QuarantineCandidateError) -> Self {
Self::Index(IndexError::QuarantineCandidate(e))
}
}
impl From<WebhookDeliveryError> for ServerError {
fn from(e: WebhookDeliveryError) -> Self {
Self::Index(IndexError::WebhookDelivery(e))
}
}
impl From<FileRecordInvariantError> for ServerError {
fn from(e: FileRecordInvariantError) -> Self {
Self::Index(IndexError::FileRecordInvariant(e))
}
}
impl From<InvalidLifecycleMetadataError> for ServerError {
fn from(e: InvalidLifecycleMetadataError) -> Self {
Self::Index(IndexError::InvalidLifecycleMetadata(e))
}
}
impl From<InvalidReconstructionResponseError> for ServerError {
fn from(e: InvalidReconstructionResponseError) -> Self {
Self::Index(IndexError::InvalidReconstructionResponse(e))
}
}
impl From<ParseStoredFileRecordError> for ServerError {
fn from(e: ParseStoredFileRecordError) -> Self {
match e {
ParseStoredFileRecordError::StoredFileMetadataTooLarge {
observed_bytes,
maximum_bytes,
} => Self::StoredFileMetadataTooLarge {
observed_bytes,
maximum_bytes,
},
ParseStoredFileRecordError::Json(e) => Self::Json(e),
}
}
}
#[derive(Debug, Serialize)]
struct ErrorBody {
error: String,
}
#[cfg(test)]
mod tests {
use axum::http::StatusCode;
use super::ServerError;
fn status_for(error: &ServerError) -> StatusCode {
error.status_code()
}
#[test]
fn not_found_maps_to_404() {
assert_eq!(status_for(&ServerError::NotFound), StatusCode::NOT_FOUND);
}
#[test]
fn insufficient_scope_maps_to_403() {
assert_eq!(
status_for(&ServerError::InsufficientScope),
StatusCode::FORBIDDEN
);
}
#[test]
fn too_many_upload_sessions_maps_to_429() {
assert_eq!(
status_for(&ServerError::TooManyUploadSessions),
StatusCode::TOO_MANY_REQUESTS
);
}
#[test]
fn too_many_registry_token_requests_maps_to_429() {
assert_eq!(
status_for(&ServerError::TooManyRegistryTokenRequests),
StatusCode::TOO_MANY_REQUESTS
);
}
#[test]
fn missing_authorization_maps_to_401() {
assert_eq!(
status_for(&ServerError::MissingAuthorization),
StatusCode::UNAUTHORIZED
);
}
#[test]
fn request_body_too_large_maps_to_413() {
assert_eq!(
status_for(&ServerError::RequestBodyTooLarge),
StatusCode::PAYLOAD_TOO_LARGE
);
}
#[test]
fn invalid_file_id_maps_to_400() {
assert_eq!(
status_for(&ServerError::InvalidFileId),
StatusCode::BAD_REQUEST
);
}
#[test]
fn range_not_satisfiable_maps_to_416() {
assert_eq!(
status_for(&ServerError::RangeNotSatisfiable),
StatusCode::RANGE_NOT_SATISFIABLE
);
}
#[test]
fn not_acceptable_maps_to_406() {
assert_eq!(
status_for(&ServerError::NotAcceptable),
StatusCode::NOT_ACCEPTABLE
);
}
#[test]
fn io_error_maps_to_500() {
let io_err = std::io::Error::other("test");
assert_eq!(
status_for(&ServerError::Io(io_err)),
StatusCode::INTERNAL_SERVER_ERROR
);
}
#[test]
fn transfer_limiter_closed_maps_to_503() {
assert_eq!(
status_for(&ServerError::TransferLimiterClosed),
StatusCode::SERVICE_UNAVAILABLE
);
}
#[test]
fn provider_denied_maps_to_403() {
assert_eq!(
status_for(&ServerError::ProviderDenied),
StatusCode::FORBIDDEN
);
}
#[test]
fn unknown_provider_maps_to_404() {
assert_eq!(
status_for(&ServerError::UnknownProvider),
StatusCode::NOT_FOUND
);
}
#[test]
fn overflow_maps_to_500() {
assert_eq!(
status_for(&ServerError::Overflow),
StatusCode::INTERNAL_SERVER_ERROR
);
}
#[test]
fn invalid_content_hash_maps_to_400() {
assert_eq!(
status_for(&ServerError::InvalidContentHash),
StatusCode::BAD_REQUEST
);
}
#[test]
fn expected_body_hash_mismatch_maps_to_400() {
assert_eq!(
status_for(&ServerError::ExpectedBodyHashMismatch),
StatusCode::BAD_REQUEST
);
}
}