parse_rust_core/error.rs
1//! Parse error codes.
2//!
3//! **Error codes are API.** Every variant carries the upstream numeric code from
4//! `src/Error.js`, and `spec/` asserts on these numbers directly. Never invent a code and
5//! never change one to a better-fitting one.
6//!
7//! Codes extracted from the `parse` npm SDK bundled with parse-server 9.10.1-alpha.6, which is
8//! the same table `src/Error.js` re-exports.
9
10use std::fmt;
11
12/// The upstream error code table. The discriminant *is* the wire value.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14#[repr(i32)]
15#[non_exhaustive]
16pub enum ErrorCode {
17 OtherCause = -1,
18 InternalServerError = 1,
19 ConnectionFailed = 100,
20 ObjectNotFound = 101,
21 InvalidQuery = 102,
22 InvalidClassName = 103,
23 MissingObjectId = 104,
24 InvalidKeyName = 105,
25 InvalidPointer = 106,
26 InvalidJson = 107,
27 CommandUnavailable = 108,
28 NotInitialized = 109,
29 IncorrectType = 111,
30 InvalidChannelName = 112,
31 PushMisconfigured = 115,
32 ObjectTooLarge = 116,
33 OperationForbidden = 119,
34 CacheMiss = 120,
35 InvalidNestedKey = 121,
36 InvalidFileName = 122,
37 InvalidAcl = 123,
38 Timeout = 124,
39 InvalidEmailAddress = 125,
40 MissingContentType = 126,
41 MissingContentLength = 127,
42 InvalidContentLength = 128,
43 FileTooLarge = 129,
44 FileSaveError = 130,
45 /// 135 and 136 have **no name in the `parse` SDK's error table**. Upstream throws them as
46 /// bare numbers: `new Parse.Error(135, ...)` at `SchemaController.js:508` and
47 /// `SchemasRouter.js:90`, `new Parse.Error(136, ...)` at `SchemaController.js:1242` and
48 /// three places in `RestWrite.js`. The names here are ours, chosen from the messages, and
49 /// `spec/Schema.spec.js` asserts on the numbers rather than on any constant. Do not
50 /// "correct" either to a named neighbour: the number is what a client sees.
51 MissingClassName = 135,
52 UnchangeableField = 136,
53 DuplicateValue = 137,
54 InvalidRoleName = 139,
55 ExceededQuota = 140,
56 ScriptFailed = 141,
57 ValidationError = 142,
58 InvalidImageData = 143,
59 UnsavedFileError = 151,
60 InvalidPushTimeError = 152,
61 FileDeleteError = 153,
62 RequestLimitExceeded = 155,
63 DuplicateRequest = 159,
64 InvalidEventName = 160,
65 FileDeleteUnnamedError = 161,
66 InvalidValue = 162,
67 UsernameMissing = 200,
68 PasswordMissing = 201,
69 UsernameTaken = 202,
70 EmailTaken = 203,
71 EmailMissing = 204,
72 EmailNotFound = 205,
73 SessionMissing = 206,
74 MustCreateUserThroughSignup = 207,
75 AccountAlreadyLinked = 208,
76 InvalidSessionToken = 209,
77 MfaError = 210,
78 MfaTokenRequired = 211,
79 LinkedIdMissing = 250,
80 InvalidLinkedSession = 251,
81 UnsupportedService = 252,
82 InvalidSchemaOperation = 255,
83 AggregateError = 600,
84 FileReadError = 601,
85 XDomainRequest = 602,
86}
87
88impl ErrorCode {
89 /// The wire value. This is what goes in the `code` field of an error body.
90 pub fn as_i32(self) -> i32 {
91 self as i32
92 }
93}
94
95impl fmt::Display for ErrorCode {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 write!(f, "{}", self.as_i32())
98 }
99}
100
101/// Which upstream throw an error corresponds to, and therefore which body a client sees.
102///
103/// `handleParseErrors` branches on the **type** of the thrown value rather than on its code
104/// (`middlewares.js:596-646`). A `Parse.Error` renders its own message; anything else renders a
105/// fixed one and the detail goes only to the log. So the same detail is a disclosure or not
106/// depending on which of the two it travelled in, and the code alone cannot tell them apart:
107/// upstream throws `Parse.Error(INTERNAL_SERVER_ERROR, ...)` deliberately in several places
108/// (`Auth.js:195`, `DatabaseController.js:1590-1595`) and those keep their messages.
109///
110/// **No `Default` impl, deliberately.** A forgotten field would select the disclosing variant,
111/// which is the failure this enum exists to prevent. Every value is chosen by a constructor.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum ErrorOrigin {
114 /// A `Parse.Error`. The message is wire-visible.
115 Parse,
116 /// A bare `Error` upstream. The message is server-side detail and never reaches a client.
117 Internal,
118}
119
120/// Out-of-band data an error carries for the server's own use.
121///
122/// **Nothing here is ever serialized into a response body.** Upstream's equivalent is
123/// `err.userInfo`, which the error middleware never renders: it writes `code` and `message` and
124/// nothing else (`middlewares.js:617`).
125///
126/// The point of the type is that the alternative is worse. Recovering which unique index collided
127/// by leaving the driver's text in `message` and parsing it downstream puts the database name and
128/// the colliding value on the wire, which is the disclosure this replaces.
129#[derive(Debug, Clone, Default, PartialEq, Eq)]
130pub struct ParseErrorInfo {
131 /// `err.userInfo.duplicated_field` (`MongoStorageAdapter.js:584`). The field whose unique
132 /// index a write collided on.
133 pub duplicated_field: Option<String>,
134}
135
136/// A Parse error: a code plus a message, plus how much of it may be seen.
137///
138/// No `source` chaining and no automatic `From` conversions from I/O or driver errors. That is
139/// deliberate: mapping a storage failure onto a Parse code is a decision each adapter must make
140/// explicitly, because picking the wrong code is a wire-compatibility bug that no type system
141/// will catch.
142#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
143#[error("{code}: {message}")]
144pub struct ParseError {
145 pub code: ErrorCode,
146 pub message: String,
147 /// Which envelope this becomes. See [`ErrorOrigin`].
148 pub origin: ErrorOrigin,
149 /// Data for the server, never for the client. See [`ParseErrorInfo`].
150 pub info: ParseErrorInfo,
151}
152
153impl ParseError {
154 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
155 Self {
156 code,
157 message: message.into(),
158 origin: ErrorOrigin::Parse,
159 info: ParseErrorInfo::default(),
160 }
161 }
162
163 /// A failure upstream throws as a bare `Error`, so its detail never reaches a client.
164 ///
165 /// The client sees `{"code":1,"message":"Internal server error."}` whatever `detail` says
166 /// (`middlewares.js:636-644`); `detail` is logged and is the only record of what happened.
167 /// Write it for an operator reading a log, not for an SDK.
168 ///
169 /// Use this wherever upstream throws a plain `Error`, and `new(ErrorCode::InternalServerError,
170 /// ..)` where upstream throws a `Parse.Error` carrying that code. The two are different bodies.
171 #[must_use]
172 pub fn internal(detail: impl Into<String>) -> Self {
173 let detail = detail.into();
174 log_detail(ErrorCode::InternalServerError, &detail);
175 Self {
176 code: ErrorCode::InternalServerError,
177 message: detail,
178 origin: ErrorOrigin::Internal,
179 info: ParseErrorInfo::default(),
180 }
181 }
182
183 /// Record which field's unique index collided, out of band.
184 #[must_use]
185 pub fn with_duplicated_field(mut self, field: impl Into<String>) -> Self {
186 self.info.duplicated_field = Some(field.into());
187 self
188 }
189
190 /// The field whose unique index collided, if the adapter could recover it.
191 pub fn duplicated_field(&self) -> Option<&str> {
192 self.info.duplicated_field.as_deref()
193 }
194}
195
196/// Whether a denial tells the client *why* it was denied.
197///
198/// `enableSanitizedErrorResponse` (`Options/Definitions.js:253-258`). Upstream's default is
199/// `true`, and its check is `config?.enableSanitizedErrorResponse !== false` (`Error.js:21`), so
200/// an absent config withholds too. [`ErrorDetail::Withheld`] is therefore what a stock deployment
201/// runs, and it is the message every unmodified SDK sees.
202///
203/// **No `Default` impl, deliberately.** The disclosing regime is a configuration decision, and a
204/// type that hands one out lets a call site acquire it by forgetting rather than by choosing.
205/// Every value of this type is derived from a config field.
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum ErrorDetail {
208 /// The client sees the generic message. The detail stays server-side.
209 Withheld,
210 /// The client sees the detailed message. `enableSanitizedErrorResponse: false`.
211 Disclosed,
212}
213
214impl ErrorDetail {
215 /// From the config flag, so the mapping lives in one place rather than at each call site.
216 pub fn from_sanitized(enable_sanitized_error_response: bool) -> Self {
217 if enable_sanitized_error_response {
218 ErrorDetail::Withheld
219 } else {
220 ErrorDetail::Disclosed
221 }
222 }
223}
224
225/// Log the detail that may be about to be withheld.
226///
227/// Upstream logs it on every call through the logger controller, in both regimes
228/// (`Error.js:15-19`), so the reason for a denial is always recoverable from the server log.
229/// parse-rust has no logger controller yet, so this follows the pattern the server crate already
230/// uses: stderr behind `PARSE_RUST_TRACE`. Structured logging through `tracing` is a later
231/// milestone, and until it lands the detail is available on demand rather than by default.
232fn log_detail(code: ErrorCode, detailed: &str) {
233 static TRACE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
234 if *TRACE.get_or_init(|| std::env::var("PARSE_RUST_TRACE").is_ok()) {
235 eprintln!("[trace] sanitized error: {code}: {detailed}");
236 }
237}
238
239/// Denials whose detail is logged and, by default, withheld.
240impl ParseError {
241 /// `createSanitizedError` (`Error.js:13-21`): the single constructor for a denial whose
242 /// detailed message upstream replaces with a generic one.
243 ///
244 /// The set of call sites is contract in both directions. Upstream routes a specific list of
245 /// errors through this function and leaves the rest alone, so an error parse-rust sanitizes
246 /// that upstream does not is as much a wire divergence as one it fails to sanitize. Adding a
247 /// call here means finding the matching `createSanitizedError` at the pin first.
248 ///
249 /// `generic` is upstream's `sanitizedMessage` parameter, which defaults to `Permission
250 /// denied` and is overridden at exactly one call site (`DatabaseController.js:1590-1595`).
251 /// It is required here rather than defaulted, because the two upstream spellings are
252 /// different strings on the wire and picking the wrong one silently is the failure this
253 /// argument exists to prevent.
254 #[must_use]
255 pub fn sanitized(
256 code: ErrorCode,
257 detailed: impl Into<String>,
258 generic: &str,
259 detail: ErrorDetail,
260 ) -> Self {
261 let detailed = detailed.into();
262 log_detail(code, &detailed);
263 match detail {
264 ErrorDetail::Withheld => Self::new(code, generic),
265 ErrorDetail::Disclosed => Self::new(code, detailed),
266 }
267 }
268
269 /// The common case: upstream's default `sanitizedMessage`.
270 #[must_use]
271 pub fn permission_denied(
272 code: ErrorCode,
273 detailed: impl Into<String>,
274 detail: ErrorDetail,
275 ) -> Self {
276 Self::sanitized(code, detailed, PERMISSION_DENIED, detail)
277 }
278}
279
280/// `createSanitizedError`'s default `sanitizedMessage` (`Error.js:13`), and the whole of
281/// `createSanitizedHttpError`'s (`Error.js:41`).
282pub const PERMISSION_DENIED: &str = "Permission denied";
283
284/// The one message a duplicate-key collision ever carries (`MongoStorageAdapter.js:576-579`).
285///
286/// Fixed, and the same string in both upstream adapters. Which field collided travels in
287/// [`ParseErrorInfo::duplicated_field`] instead, because the driver's own text names the database
288/// and the value that collided.
289pub const DUPLICATE_VALUE_MESSAGE: &str =
290 "A duplicate value for a field with unique values was provided";
291
292/// Convenience constructors for the codes used most often in the core.
293impl ParseError {
294 pub fn invalid_json(message: impl Into<String>) -> Self {
295 Self::new(ErrorCode::InvalidJson, message)
296 }
297 pub fn incorrect_type(message: impl Into<String>) -> Self {
298 Self::new(ErrorCode::IncorrectType, message)
299 }
300 pub fn invalid_key_name(message: impl Into<String>) -> Self {
301 Self::new(ErrorCode::InvalidKeyName, message)
302 }
303 pub fn invalid_acl(message: impl Into<String>) -> Self {
304 Self::new(ErrorCode::InvalidAcl, message)
305 }
306 pub fn invalid_query(message: impl Into<String>) -> Self {
307 Self::new(ErrorCode::InvalidQuery, message)
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn discriminants_match_upstream() {
317 // Spot-check the ones the rest of this workspace leans on, plus the two that
318 // are easy to transpose.
319 assert_eq!(ErrorCode::OtherCause.as_i32(), -1);
320 assert_eq!(ErrorCode::InternalServerError.as_i32(), 1);
321 assert_eq!(ErrorCode::ObjectNotFound.as_i32(), 101);
322 assert_eq!(ErrorCode::InvalidQuery.as_i32(), 102);
323 assert_eq!(ErrorCode::IncorrectType.as_i32(), 111);
324 assert_eq!(ErrorCode::OperationForbidden.as_i32(), 119);
325 assert_eq!(ErrorCode::DuplicateValue.as_i32(), 137);
326 assert_eq!(ErrorCode::ScriptFailed.as_i32(), 141);
327 assert_eq!(ErrorCode::DuplicateRequest.as_i32(), 159);
328 assert_eq!(ErrorCode::InvalidSessionToken.as_i32(), 209);
329 assert_eq!(ErrorCode::InvalidSchemaOperation.as_i32(), 255);
330 // 110 and 113 do not exist upstream; there is no variant to assert, and adding one
331 // would be inventing a code.
332 }
333
334 #[test]
335 fn display_is_the_number() {
336 assert_eq!(ErrorCode::ObjectNotFound.to_string(), "101");
337 }
338
339 /// The default regime is the withholding one, because upstream's default is `true`.
340 #[test]
341 fn the_two_regimes_produce_the_two_upstream_messages() {
342 let detailed = "Permission denied for action find on class Post.";
343 assert_eq!(
344 ParseError::permission_denied(
345 ErrorCode::OperationForbidden,
346 detailed,
347 ErrorDetail::from_sanitized(true)
348 )
349 .message,
350 "Permission denied"
351 );
352 assert_eq!(
353 ParseError::permission_denied(
354 ErrorCode::OperationForbidden,
355 detailed,
356 ErrorDetail::from_sanitized(false)
357 )
358 .message,
359 detailed
360 );
361 }
362
363 /// The code never moves. Only the message does.
364 #[test]
365 fn sanitizing_does_not_change_the_code() {
366 for detail in [ErrorDetail::Withheld, ErrorDetail::Disclosed] {
367 let e = ParseError::permission_denied(ErrorCode::ObjectNotFound, "why", detail);
368 assert_eq!(e.code, ErrorCode::ObjectNotFound);
369 }
370 }
371
372 /// The two ways to reach code 1 are different bodies, so they must be different values.
373 #[test]
374 fn an_internal_error_is_distinguishable_from_a_parse_error_carrying_code_one() {
375 let internal = ParseError::internal("pointer permissions: Post owner");
376 assert_eq!(internal.code, ErrorCode::InternalServerError);
377 assert_eq!(internal.origin, ErrorOrigin::Internal);
378
379 // `Auth.js:195` throws this one as a real `Parse.Error`, and its message is wire-visible.
380 let parse = ParseError::new(ErrorCode::InternalServerError, "Invalid object ID.");
381 assert_eq!(parse.origin, ErrorOrigin::Parse);
382 }
383
384 /// Everything built through the ordinary constructors keeps its message.
385 #[test]
386 fn the_ordinary_constructors_produce_parse_origin() {
387 for e in [
388 ParseError::new(ErrorCode::ObjectNotFound, "Object not found."),
389 ParseError::invalid_json("bad"),
390 ParseError::permission_denied(
391 ErrorCode::OperationForbidden,
392 "why",
393 ErrorDetail::Withheld,
394 ),
395 ] {
396 assert_eq!(e.origin, ErrorOrigin::Parse);
397 }
398 }
399
400 /// The duplicate-key field rides beside the message rather than inside it.
401 #[test]
402 fn the_duplicated_field_is_out_of_band() {
403 let e = ParseError::new(ErrorCode::DuplicateValue, DUPLICATE_VALUE_MESSAGE)
404 .with_duplicated_field("username");
405 assert_eq!(e.duplicated_field(), Some("username"));
406 // The name of the field is not in the message, and neither is anything else.
407 assert_eq!(e.message, DUPLICATE_VALUE_MESSAGE);
408 assert!(!e.message.contains("username"));
409 assert_eq!(ParseError::invalid_json("x").duplicated_field(), None);
410 }
411
412 /// The one call site upstream overrides the generic message at.
413 #[test]
414 fn the_generic_message_is_per_call_site() {
415 let e = ParseError::sanitized(
416 ErrorCode::InternalServerError,
417 "a driver said something specific",
418 "An internal server error occurred",
419 ErrorDetail::Withheld,
420 );
421 assert_eq!(e.message, "An internal server error occurred");
422 }
423}