mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <wpfleger@block.xyz> Signed-off-by: Will Pfleger <wpfleger@squareup.com> Signed-off-by: Will Pfleger <wpfleger96@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1fgdl5qqnh3k3f2xkqrvt7cujalhm623x4s7fdjdj5yrtp5fzjl9qrjpucw <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub16v54tttfqacx9ycvc3k0ut0npj564ahcuajzy6qjvh57ntmsf4uq4806j2 <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Will Pfleger <wpfleger96@gmail.com>
153 lines
5.3 KiB
Rust
153 lines
5.3 KiB
Rust
//! Media error types.
|
||
|
||
use axum::http::StatusCode;
|
||
use axum::response::{IntoResponse, Response};
|
||
|
||
/// Errors from media operations.
|
||
#[derive(Debug, thiserror::Error)]
|
||
pub enum MediaError {
|
||
#[error("unknown content type")]
|
||
UnknownContentType,
|
||
#[error("disallowed content type: {0}")]
|
||
DisallowedContentType(String),
|
||
#[error("file too large: {size} bytes (max {max})")]
|
||
FileTooLarge { size: u64, max: u64 },
|
||
#[error("image dimensions too large")]
|
||
ImageTooLarge,
|
||
#[error("invalid image data")]
|
||
InvalidImage,
|
||
#[error("invalid signature")]
|
||
InvalidSignature,
|
||
#[error("invalid auth event kind")]
|
||
InvalidAuthKind,
|
||
#[error("invalid auth verb")]
|
||
InvalidAuthVerb,
|
||
#[error("missing required tag: {0}")]
|
||
MissingTag(&'static str),
|
||
#[error("hash mismatch")]
|
||
HashMismatch,
|
||
#[error("server mismatch")]
|
||
ServerMismatch,
|
||
#[error("token expired")]
|
||
TokenExpired,
|
||
#[error("timestamp out of window")]
|
||
TimestampOutOfWindow,
|
||
#[error("storage error: {0}")]
|
||
StorageError(String),
|
||
#[error("internal error")]
|
||
Internal,
|
||
#[error("not found")]
|
||
NotFound,
|
||
#[error("missing authorization header")]
|
||
MissingAuth,
|
||
#[error("invalid authorization scheme")]
|
||
InvalidAuthScheme,
|
||
#[error("invalid base64 encoding")]
|
||
InvalidBase64,
|
||
#[error("invalid auth event")]
|
||
InvalidAuthEvent,
|
||
#[error("unauthorized")]
|
||
Unauthorized,
|
||
#[error("insufficient scope")]
|
||
InsufficientScope,
|
||
#[error("relay membership required")]
|
||
RelayMembershipRequired,
|
||
#[error("token revoked")]
|
||
TokenRevoked,
|
||
#[error("pubkey mismatch")]
|
||
PubkeyMismatch,
|
||
/// Video codec is not H.264 (avc1).
|
||
#[error("unsupported video codec: only H.264 (avc1) is accepted")]
|
||
WrongCodec,
|
||
/// Video duration exceeds the 600-second limit.
|
||
#[error("video too long: duration exceeds 600 seconds")]
|
||
DurationTooLong,
|
||
/// Video resolution exceeds 3840×2160.
|
||
#[error("video resolution too high: maximum is 3840x2160")]
|
||
ResolutionTooHigh,
|
||
/// MP4 moov atom appears after mdat — not fast-start.
|
||
#[error("moov atom not at front of file (not fast-start)")]
|
||
MoovNotAtFront,
|
||
/// Container is not MP4 (e.g. MOV, MKV).
|
||
#[error("unsupported container: only MP4 is accepted")]
|
||
UnsupportedContainer,
|
||
/// MP4 metadata could not be parsed.
|
||
#[error("invalid video data")]
|
||
InvalidVideo,
|
||
/// I/O error during streaming upload.
|
||
#[error("io error: {0}")]
|
||
Io(String),
|
||
}
|
||
|
||
impl From<image::ImageError> for MediaError {
|
||
fn from(_: image::ImageError) -> Self {
|
||
Self::InvalidImage
|
||
}
|
||
}
|
||
|
||
impl From<s3::error::S3Error> for MediaError {
|
||
fn from(e: s3::error::S3Error) -> Self {
|
||
Self::StorageError(e.to_string())
|
||
}
|
||
}
|
||
|
||
impl From<serde_json::Error> for MediaError {
|
||
fn from(e: serde_json::Error) -> Self {
|
||
Self::StorageError(e.to_string())
|
||
}
|
||
}
|
||
|
||
impl IntoResponse for MediaError {
|
||
fn into_response(self) -> Response {
|
||
let (status, msg) = match &self {
|
||
Self::NotFound => (StatusCode::NOT_FOUND, self.to_string()),
|
||
Self::DisallowedContentType(_) => {
|
||
(StatusCode::UNSUPPORTED_MEDIA_TYPE, self.to_string())
|
||
}
|
||
Self::FileTooLarge { .. } | Self::ImageTooLarge => {
|
||
(StatusCode::PAYLOAD_TOO_LARGE, self.to_string())
|
||
}
|
||
// All authentication failures return the same generic 401 to prevent oracle enumeration.
|
||
// InsufficientScope is intentionally 403 — it's an authorization (not authentication)
|
||
// failure and is safe to distinguish since it requires a valid identity first.
|
||
Self::MissingAuth
|
||
| Self::InvalidAuthScheme
|
||
| Self::InvalidBase64
|
||
| Self::InvalidAuthEvent
|
||
| Self::InvalidSignature
|
||
| Self::InvalidAuthKind
|
||
| Self::InvalidAuthVerb
|
||
| Self::TokenExpired
|
||
| Self::TimestampOutOfWindow
|
||
| Self::Unauthorized
|
||
| Self::TokenRevoked
|
||
| Self::PubkeyMismatch
|
||
| Self::HashMismatch
|
||
| Self::ServerMismatch
|
||
| Self::MissingTag(_) => {
|
||
tracing::warn!(error = %self, "authentication failed");
|
||
(
|
||
StatusCode::UNAUTHORIZED,
|
||
"authentication failed".to_string(),
|
||
)
|
||
}
|
||
Self::InsufficientScope => (StatusCode::FORBIDDEN, self.to_string()),
|
||
Self::RelayMembershipRequired => (StatusCode::FORBIDDEN, self.to_string()),
|
||
Self::UnsupportedContainer => (StatusCode::UNSUPPORTED_MEDIA_TYPE, self.to_string()),
|
||
Self::WrongCodec
|
||
| Self::DurationTooLong
|
||
| Self::ResolutionTooHigh
|
||
| Self::MoovNotAtFront
|
||
| Self::InvalidVideo => (StatusCode::BAD_REQUEST, self.to_string()),
|
||
Self::UnknownContentType | Self::InvalidImage => {
|
||
(StatusCode::BAD_REQUEST, self.to_string())
|
||
}
|
||
Self::Io(_) | Self::StorageError(_) | Self::Internal => {
|
||
tracing::error!(error = %self, "media storage error");
|
||
(StatusCode::INTERNAL_SERVER_ERROR, "internal error".into())
|
||
}
|
||
};
|
||
(status, axum::Json(serde_json::json!({"error": msg}))).into_response()
|
||
}
|
||
}
|