Skip to main content

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}