1use http::StatusCode;
2use thiserror::Error;
3
4#[derive(Debug, Error)]
5pub enum TusError {
6 #[error("missing Tus-Resumable header")]
8 MissingTusResumable,
9
10 #[error("unsupported tus version: {version}")]
11 UnsupportedVersion { version: String },
12
13 #[error("missing Upload-Offset header")]
14 MissingUploadOffset,
15
16 #[error("upload offset mismatch: expected {expected}, got {actual}")]
17 OffsetMismatch { expected: u64, actual: u64 },
18
19 #[error("wrong Content-Type: expected application/offset+octet-stream, got {0}")]
20 WrongContentType(String),
21
22 #[error("upload not found: {0}")]
23 NotFound(String),
24
25 #[error("upload has expired")]
26 Gone,
27
28 #[error("upload size exceeds server maximum of {max} bytes")]
29 EntityTooLarge { max: u64 },
30
31 #[error("chunk exceeds declared upload length (declared {declared} bytes, end offset would be {end})")]
32 ExceedsUploadLength { declared: u64, end: u64 },
33
34 #[error("checksum mismatch")]
35 ChecksumMismatch,
36
37 #[error("unsupported checksum algorithm: {0}")]
38 UnsupportedChecksumAlgorithm(String),
39
40 #[error("missing Upload-Length (or Upload-Defer-Length)")]
41 MissingUploadLength,
42
43 #[error("Upload-Length cannot be changed once set")]
44 UploadLengthAlreadySet,
45
46 #[error("extension not enabled: {0}")]
47 ExtensionNotEnabled(&'static str),
48
49 #[error("invalid metadata: {0}")]
50 InvalidMetadata(String),
51
52 #[error("invalid upload ID")]
53 InvalidUploadId,
54
55 #[error("concatenation requires at least one partial upload URL")]
56 EmptyConcatenation,
57
58 #[error("partial upload {0} is not yet complete")]
59 PartialUploadIncomplete(String),
60
61 #[error("PATCH is not allowed on a final concatenated upload")]
62 PatchOnFinalUpload,
63
64 #[error("upload is not complete or not available for download")]
65 UploadNotReadyForDownload,
66
67 #[error("method not allowed")]
68 MethodNotAllowed,
69
70 #[error("lock acquisition timed out for upload {0}")]
72 LockTimeout(String),
73
74 #[error("lock is already held for upload {0}")]
75 LockConflict(String),
76
77 #[error("hook rejected request: {0}")]
79 HookRejected(String),
80
81 #[error("I/O error: {0}")]
83 Io(#[from] std::io::Error),
84
85 #[error("serialization error: {0}")]
86 Serialization(#[from] serde_json::Error),
87
88 #[error("internal error: {0}")]
89 Internal(String),
90}
91
92impl TusError {
93 pub fn client_message(&self) -> String {
94 match self {
95 Self::Io(_) | Self::Serialization(_) | Self::Internal(_) => {
96 "internal server error".to_string()
97 }
98 _ => self.to_string(),
99 }
100 }
101
102 pub fn status_code(&self) -> StatusCode {
104 match self {
105 Self::MissingTusResumable => StatusCode::PRECONDITION_FAILED,
106 Self::UnsupportedVersion { .. } => StatusCode::PRECONDITION_FAILED,
107 Self::MissingUploadOffset => StatusCode::BAD_REQUEST,
108 Self::OffsetMismatch { .. } => StatusCode::CONFLICT,
109 Self::WrongContentType(_) => StatusCode::UNSUPPORTED_MEDIA_TYPE,
110 Self::NotFound(_) => StatusCode::NOT_FOUND,
111 Self::Gone => StatusCode::GONE,
112 Self::EntityTooLarge { .. } => StatusCode::PAYLOAD_TOO_LARGE,
113 Self::ExceedsUploadLength { .. } => StatusCode::PAYLOAD_TOO_LARGE,
114 Self::ChecksumMismatch => match StatusCode::from_u16(460) {
116 Ok(s) => s,
117 Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
118 },
119 Self::UnsupportedChecksumAlgorithm(_) => StatusCode::BAD_REQUEST,
120 Self::MissingUploadLength => StatusCode::BAD_REQUEST,
121 Self::UploadLengthAlreadySet => StatusCode::BAD_REQUEST,
122 Self::ExtensionNotEnabled(_) => StatusCode::NOT_FOUND,
123 Self::InvalidMetadata(_) => StatusCode::BAD_REQUEST,
124 Self::InvalidUploadId => StatusCode::BAD_REQUEST,
125 Self::EmptyConcatenation => StatusCode::BAD_REQUEST,
126 Self::PartialUploadIncomplete(_) => StatusCode::BAD_REQUEST,
127 Self::PatchOnFinalUpload => StatusCode::FORBIDDEN,
128 Self::UploadNotReadyForDownload => StatusCode::BAD_REQUEST,
129 Self::MethodNotAllowed => StatusCode::METHOD_NOT_ALLOWED,
130 Self::LockTimeout(_) => StatusCode::REQUEST_TIMEOUT,
131 Self::LockConflict(_) => StatusCode::LOCKED,
132 Self::HookRejected(_) => StatusCode::FORBIDDEN,
133 Self::Io(_) => StatusCode::INTERNAL_SERVER_ERROR,
134 Self::Serialization(_) => StatusCode::INTERNAL_SERVER_ERROR,
135 Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
136 }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
146 fn status_code_mapping() {
147 let cases: &[(TusError, u16)] = &[
148 (TusError::MissingTusResumable, 412),
149 (
150 TusError::UnsupportedVersion {
151 version: "0.9".into(),
152 },
153 412,
154 ),
155 (TusError::MissingUploadOffset, 400),
156 (
157 TusError::OffsetMismatch {
158 expected: 10,
159 actual: 5,
160 },
161 409,
162 ),
163 (TusError::WrongContentType("text/plain".into()), 415),
164 (TusError::NotFound("abc".into()), 404),
165 (TusError::Gone, 410),
166 (TusError::EntityTooLarge { max: 1024 }, 413),
167 (
168 TusError::ExceedsUploadLength {
169 declared: 10,
170 end: 20,
171 },
172 413,
173 ),
174 (TusError::ChecksumMismatch, 460),
175 (TusError::UnsupportedChecksumAlgorithm("crc32".into()), 400),
176 (TusError::MissingUploadLength, 400),
177 (TusError::UploadLengthAlreadySet, 400),
178 (TusError::ExtensionNotEnabled("concatenation"), 404),
179 (TusError::InvalidMetadata("bad base64".into()), 400),
180 (TusError::InvalidUploadId, 400),
181 (TusError::EmptyConcatenation, 400),
182 (TusError::PartialUploadIncomplete("id1".into()), 400),
183 (TusError::PatchOnFinalUpload, 403),
184 (TusError::UploadNotReadyForDownload, 400),
185 (TusError::MethodNotAllowed, 405),
186 (TusError::LockTimeout("id1".into()), 408),
187 (TusError::LockConflict("id1".into()), 423),
188 (TusError::HookRejected("not allowed".into()), 403),
189 (TusError::Io(std::io::Error::other("disk full")), 500),
190 (
191 TusError::Serialization(serde_json::from_str::<()>("!").unwrap_err()),
192 500,
193 ),
194 (TusError::Internal("oops".into()), 500),
195 ];
196
197 for (err, expected_status) in cases {
198 assert_eq!(
199 err.status_code().as_u16(),
200 *expected_status,
201 "wrong status for: {err}"
202 );
203 }
204 }
205}