Skip to main content

jmap_types/
error.rs

1//! RFC 8620 §3.6 JMAP method-level error type ([`JmapError`],
2//! [`MethodErrorType`]).
3
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7/// The machine-readable `type` of a [`JmapError`].
8///
9/// Known variants are the RFC 8620 request-level error types (§3.6.1,
10/// whose wire form in an RFC 7807 problem-details body is prefixed with
11/// `urn:ietf:params:jmap:error:`), the §3.6.2 method-level error types,
12/// and the method-specific errors of §5.1 to §5.6. Every other wire
13/// string, including extension errors such as `"rateLimited"`, is
14/// [`Self::Other`].
15///
16/// `From<&str>` and `Deserialize` share one mapping: a known wire string
17/// always builds its typed variant, never `Other`.
18#[non_exhaustive]
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
20pub enum MethodErrorType {
21    /// RFC 8620 §3.6.1 — the request used an unsupported capability.
22    UnknownCapability,
23    /// RFC 8620 §3.6.1 — the content type was not `application/json` or
24    /// the request did not parse as I-JSON.
25    NotJson,
26    /// RFC 8620 §3.6.1 — the request parsed as JSON but not as a Request
27    /// object.
28    NotRequest,
29    /// RFC 8620 §3.6.1 — the request exceeded a server limit; the limit's
30    /// name is [`JmapError::limit_name`].
31    Limit,
32    /// RFC 8620 §3.6.2 — an internal server resource was temporarily
33    /// unavailable.
34    ServerUnavailable,
35    /// RFC 8620 §3.6.2 — an unexpected error occurred.
36    ServerFail,
37    /// RFC 8620 §3.6.2 — some, but not all, expected changes were made.
38    ServerPartialFail,
39    /// RFC 8620 §3.6.2 — the server does not recognise this method name.
40    UnknownMethod,
41    /// RFC 8620 §3.6.2 — an argument is of the wrong type or invalid.
42    InvalidArguments,
43    /// RFC 8620 §3.6.2 — a result reference failed to resolve.
44    InvalidResultReference,
45    /// RFC 8620 §3.6.2 — the method or arguments are forbidden.
46    Forbidden,
47    /// RFC 8620 §3.6.2 — the `accountId` does not correspond to a valid
48    /// account.
49    AccountNotFound,
50    /// RFC 8620 §3.6.2 — the account does not support this method.
51    AccountNotSupportedByMethod,
52    /// RFC 8620 §3.6.2 — the account is read-only.
53    AccountReadOnly,
54    /// RFC 8620 §5.1 and §5.3 — too many ids or objects in one call.
55    RequestTooLarge,
56    /// RFC 8620 §5.2 and §5.6 — the server cannot calculate the changes
57    /// from the given state.
58    CannotCalculateChanges,
59    /// RFC 8620 §5.3 and §5.4 — `ifInState` did not match the current
60    /// state.
61    StateMismatch,
62    /// RFC 8620 §5.4 — the `fromAccountId` does not correspond to a valid
63    /// account.
64    FromAccountNotFound,
65    /// RFC 8620 §5.4 — the `fromAccountId` account does not support this
66    /// method.
67    FromAccountNotSupportedByMethod,
68    /// RFC 8620 §5.5 — the anchor was not found in the results.
69    AnchorNotFound,
70    /// RFC 8620 §5.5 — the sort is syntactically valid but unsupported.
71    UnsupportedSort,
72    /// RFC 8620 §5.5 — the filter is syntactically valid but unsupported.
73    UnsupportedFilter,
74    /// RFC 8620 §5.6 — there are more changes than `maxChanges`.
75    TooManyChanges,
76    /// Any other error type, such as an extension's.
77    Other(String),
78}
79
80crate::impl_string_enum!(
81    MethodErrorType,
82    "a JMAP method error type string",
83    "unknownCapability" => UnknownCapability,
84    "notJSON" => NotJson,
85    "notRequest" => NotRequest,
86    "limit" => Limit,
87    "serverUnavailable" => ServerUnavailable,
88    "serverFail" => ServerFail,
89    "serverPartialFail" => ServerPartialFail,
90    "unknownMethod" => UnknownMethod,
91    "invalidArguments" => InvalidArguments,
92    "invalidResultReference" => InvalidResultReference,
93    "forbidden" => Forbidden,
94    "accountNotFound" => AccountNotFound,
95    "accountNotSupportedByMethod" => AccountNotSupportedByMethod,
96    "accountReadOnly" => AccountReadOnly,
97    "requestTooLarge" => RequestTooLarge,
98    "cannotCalculateChanges" => CannotCalculateChanges,
99    "stateMismatch" => StateMismatch,
100    "fromAccountNotFound" => FromAccountNotFound,
101    "fromAccountNotSupportedByMethod" => FromAccountNotSupportedByMethod,
102    "anchorNotFound" => AnchorNotFound,
103    "unsupportedSort" => UnsupportedSort,
104    "unsupportedFilter" => UnsupportedFilter,
105    "tooManyChanges" => TooManyChanges,
106);
107
108/// JMAP method-level error, serializable for inclusion in `methodResponses`.
109///
110/// See RFC 8620 §3.6.2 for the standard error types.
111/// The JSON key is `"type"` (not `"error_type"`) per RFC 8620.
112#[non_exhaustive]
113#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)]
114#[error("{error_type}")]
115pub struct JmapError {
116    /// Error type per RFC 8620 §3.6.
117    #[serde(rename = "type")]
118    pub error_type: MethodErrorType,
119    /// Human-readable description. Omitted from JSON when `None`.
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub description: Option<String>,
122    /// Maximum `maxChanges` value the server will accept. Only set for `"tooManyChanges"` (RFC 8620 §9.6.1 MUST).
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub limit: Option<u64>,
125    /// Name of the exceeded limit for a request-level `limit` error
126    /// (RFC 8620 §3.6.1: "the "limit" property MUST contain the name of
127    /// the limit being applied"), e.g. `"maxCallsInRequest"`.
128    ///
129    /// Request-level errors are sent as an RFC 7807 problem-details body,
130    /// where `jmap-server` writes this as the `"limit"` property. It never
131    /// appears in a `methodResponses` error, so it is not serialized here
132    /// (the `limit` key there is the `tooManyChanges` count).
133    #[serde(skip)]
134    pub limit_name: Option<String>,
135}
136
137impl JmapError {
138    /// RFC 8620 §3.6.2 — "invalidArguments"
139    pub fn invalid_arguments(desc: impl Into<String>) -> Self {
140        Self {
141            error_type: MethodErrorType::InvalidArguments,
142            description: Some(desc.into()),
143            limit: None,
144            limit_name: None,
145        }
146    }
147
148    /// RFC 8620 §3.6.2 — "forbidden"
149    pub fn forbidden() -> Self {
150        Self {
151            error_type: MethodErrorType::Forbidden,
152            description: None,
153            limit: None,
154            limit_name: None,
155        }
156    }
157
158    /// RFC 8620 §5.1 — "accountNotFound"
159    pub fn account_not_found() -> Self {
160        Self {
161            error_type: MethodErrorType::AccountNotFound,
162            description: None,
163            limit: None,
164            limit_name: None,
165        }
166    }
167
168    /// RFC 8620 §5.1 — "accountNotSupportedByMethod"
169    pub fn account_not_supported_by_method() -> Self {
170        Self {
171            error_type: MethodErrorType::AccountNotSupportedByMethod,
172            description: None,
173            limit: None,
174            limit_name: None,
175        }
176    }
177
178    /// RFC 8620 §5.1 — "accountReadOnly"
179    pub fn account_read_only() -> Self {
180        Self {
181            error_type: MethodErrorType::AccountReadOnly,
182            description: None,
183            limit: None,
184            limit_name: None,
185        }
186    }
187
188    /// RFC 8620 §3.6.2 — "serverUnavailable"
189    pub fn server_unavailable() -> Self {
190        Self {
191            error_type: MethodErrorType::ServerUnavailable,
192            description: None,
193            limit: None,
194            limit_name: None,
195        }
196    }
197
198    /// RFC 8620 §3.6.2 — "serverFail"
199    pub fn server_fail(desc: impl Into<String>) -> Self {
200        Self {
201            error_type: MethodErrorType::ServerFail,
202            description: Some(desc.into()),
203            limit: None,
204            limit_name: None,
205        }
206    }
207
208    /// RFC 8620 §3.6.2 — "serverPartialFail"
209    pub fn server_partial_fail() -> Self {
210        Self {
211            error_type: MethodErrorType::ServerPartialFail,
212            description: None,
213            limit: None,
214            limit_name: None,
215        }
216    }
217
218    /// RFC 8620 §3.6.2 — "unknownMethod"
219    pub fn unknown_method() -> Self {
220        Self {
221            error_type: MethodErrorType::UnknownMethod,
222            description: None,
223            limit: None,
224            limit_name: None,
225        }
226    }
227
228    /// RFC 8620 §3.6.2 — "invalidResultReference"
229    pub fn invalid_result_reference() -> Self {
230        Self {
231            error_type: MethodErrorType::InvalidResultReference,
232            description: None,
233            limit: None,
234            limit_name: None,
235        }
236    }
237
238    /// RFC 8620 §5.2 and §5.6 — "cannotCalculateChanges"
239    pub fn cannot_calculate_changes() -> Self {
240        Self {
241            error_type: MethodErrorType::CannotCalculateChanges,
242            description: None,
243            limit: None,
244            limit_name: None,
245        }
246    }
247
248    /// RFC 8620 §5.3 — "stateMismatch"
249    pub fn state_mismatch() -> Self {
250        Self {
251            error_type: MethodErrorType::StateMismatch,
252            description: None,
253            limit: None,
254            limit_name: None,
255        }
256    }
257
258    /// "tooLarge" — a method-level error of `Principal/getAvailability`
259    /// (draft-ietf-jmap-calendars §2.2); RFC 8620 §5.3 defines it only as
260    /// a SetError.
261    pub fn too_large() -> Self {
262        Self {
263            error_type: MethodErrorType::from("tooLarge"),
264            description: None,
265            limit: None,
266            limit_name: None,
267        }
268    }
269
270    /// RFC 8620 §5.1 and §5.3 — "requestTooLarge"
271    pub fn request_too_large() -> Self {
272        Self {
273            error_type: MethodErrorType::RequestTooLarge,
274            description: None,
275            limit: None,
276            limit_name: None,
277        }
278    }
279
280    /// "rateLimit" — a method-level error of `Principal/getAvailability`
281    /// (draft-ietf-jmap-calendars §2.2); RFC 8620 §5.3 defines it only as
282    /// a SetError.
283    pub fn rate_limit() -> Self {
284        Self {
285            error_type: MethodErrorType::from("rateLimit"),
286            description: None,
287            limit: None,
288            limit_name: None,
289        }
290    }
291
292    /// RFC 8620 §5.5 — "unsupportedFilter"
293    pub fn unsupported_filter() -> Self {
294        Self {
295            error_type: MethodErrorType::UnsupportedFilter,
296            description: None,
297            limit: None,
298            limit_name: None,
299        }
300    }
301
302    /// RFC 8620 §5.5 — "anchorNotFound"
303    pub fn anchor_not_found() -> Self {
304        Self {
305            error_type: MethodErrorType::AnchorNotFound,
306            description: None,
307            limit: None,
308            limit_name: None,
309        }
310    }
311
312    /// RFC 8620 §5.4 — "fromAccountNotFound"
313    pub fn from_account_not_found() -> Self {
314        Self {
315            error_type: MethodErrorType::FromAccountNotFound,
316            description: None,
317            limit: None,
318            limit_name: None,
319        }
320    }
321
322    /// RFC 8620 §5.4 — "fromAccountNotSupportedByMethod"
323    pub fn from_account_not_supported_by_method() -> Self {
324        Self {
325            error_type: MethodErrorType::FromAccountNotSupportedByMethod,
326            description: None,
327            limit: None,
328            limit_name: None,
329        }
330    }
331
332    /// RFC 8620 §5.5 — "unsupportedSort"
333    pub fn unsupported_sort() -> Self {
334        Self {
335            error_type: MethodErrorType::UnsupportedSort,
336            description: None,
337            limit: None,
338            limit_name: None,
339        }
340    }
341
342    /// Returns a `tooManyChanges` error with the server's limit included.
343    ///
344    /// Per RFC 8620 §9.6.1, the `limit` field MUST be present so the client
345    /// knows the maximum `maxChanges` value to use on retry.
346    pub fn too_many_changes_with_limit(limit: u64) -> Self {
347        Self {
348            error_type: MethodErrorType::TooManyChanges,
349            description: None,
350            limit: Some(limit),
351            limit_name: None,
352        }
353    }
354
355    /// RFC 8620 §3.6.1 — "notJSON" (request-level error)
356    ///
357    /// The request body was not valid JSON or did not have `application/json` content type.
358    pub fn not_json() -> Self {
359        Self {
360            error_type: MethodErrorType::NotJson,
361            description: None,
362            limit: None,
363            limit_name: None,
364        }
365    }
366
367    /// RFC 8620 §3.6.1 — "notRequest" (request-level error)
368    ///
369    /// The request parsed as JSON but did not match the JMAP Request object shape.
370    pub fn not_request() -> Self {
371        Self {
372            error_type: MethodErrorType::NotRequest,
373            description: None,
374            limit: None,
375            limit_name: None,
376        }
377    }
378
379    /// RFC 8620 §3.6.1 — "limit" (request-level error)
380    ///
381    /// The request was rejected because it would exceed a capability limit such as
382    /// `maxCallsInRequest` or `maxSizeRequest`.
383    ///
384    /// `limit_name` is the name of the exceeded limit (e.g. `"maxCallsInRequest"`),
385    /// stored in [`limit_name`][JmapError::limit_name]. The HTTP layer
386    /// (`jmap-server::RequestError`) writes it as the RFC-required `"limit"`
387    /// property of the RFC 7807 problem-details body.
388    pub fn limit(limit_name: impl Into<String>) -> Self {
389        Self {
390            error_type: MethodErrorType::Limit,
391            description: None,
392            limit: None,
393            limit_name: Some(limit_name.into()),
394        }
395    }
396
397    /// "unknownCapability" with the failing URI surfaced to the client.
398    ///
399    /// The URI is stored in [`description`][JmapError::description].  The HTTP layer
400    /// (`jmap-server::RequestError`) reads `description` to populate the `"detail"` field
401    /// in the RFC 7807 problem-details response body — the same mechanism used by
402    /// [`limit()`][Self::limit] for its limit-name payload.
403    ///
404    /// **Invariant**: always construct unknownCapability errors that carry a URI with this
405    /// function, never by setting `description` manually.  A missing or incorrect description
406    /// means the client never learns which capability it requested that the server does not
407    /// support.
408    pub fn unknown_capability_with_detail(uri: impl Into<String>) -> Self {
409        Self {
410            error_type: MethodErrorType::UnknownCapability,
411            description: Some(uri.into()),
412            limit: None,
413            limit_name: None,
414        }
415    }
416
417    /// Create a `JmapError` with an extension error type such as
418    /// `"rateLimited"`.
419    ///
420    /// A string naming a known type builds that variant
421    /// (`JmapError::custom("forbidden")` equals [`JmapError::forbidden`]).
422    pub fn custom(error_type: impl Into<MethodErrorType>) -> Self {
423        Self {
424            error_type: error_type.into(),
425            description: None,
426            limit: None,
427            limit_name: None,
428        }
429    }
430}
431
432/// Name the top-level JSON type of `v` (`null`, `boolean`, `number`,
433/// `string`, `array` or `object`) for an error description.
434///
435/// Malformed-argument errors interpolate this tag instead of the value
436/// itself. `Value`'s `Display` re-serialises arbitrary client-supplied JSON,
437/// so echoing it would let a request amplify its own error response and
438/// copy attacker-controlled text into the `description` that RFC 8620
439/// §3.6.1 reserves for "unstructured English text suitable for displaying
440/// to a developer" (and into any log that buffers responses).
441#[must_use]
442pub fn json_type_name(v: &serde_json::Value) -> &'static str {
443    match v {
444        serde_json::Value::Null => "null",
445        serde_json::Value::Bool(_) => "boolean",
446        serde_json::Value::Number(_) => "number",
447        serde_json::Value::String(_) => "string",
448        serde_json::Value::Array(_) => "array",
449        serde_json::Value::Object(_) => "object",
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456
457    // Independent oracle: RFC 8620 §3.6.2 and §5.x specify these exact type strings.
458
459    #[test]
460    fn invalid_arguments_serializes_type_and_description() {
461        let e = JmapError::invalid_arguments("ids field is required");
462        let json = serde_json::to_string(&e).unwrap();
463        assert!(
464            json.contains("\"type\""),
465            "must use 'type' key per RFC 8620"
466        );
467        assert!(json.contains("\"invalidArguments\""));
468        assert!(json.contains("\"description\""));
469        assert!(json.contains("ids field is required"));
470    }
471
472    #[test]
473    fn forbidden_omits_description() {
474        let e = JmapError::forbidden();
475        let json = serde_json::to_string(&e).unwrap();
476        assert!(json.contains("\"forbidden\""));
477        assert!(
478            !json.contains("\"description\""),
479            "None description must be omitted"
480        );
481    }
482
483    #[test]
484    fn account_not_found_type_string() {
485        let e = JmapError::account_not_found();
486        let json = serde_json::to_string(&e).unwrap();
487        assert!(json.contains("\"accountNotFound\""));
488        assert!(!json.contains("\"description\""));
489    }
490
491    #[test]
492    fn account_not_supported_by_method_type_string() {
493        let e = JmapError::account_not_supported_by_method();
494        let json = serde_json::to_string(&e).unwrap();
495        assert!(json.contains("\"accountNotSupportedByMethod\""));
496    }
497
498    #[test]
499    fn account_read_only_type_string() {
500        let e = JmapError::account_read_only();
501        let json = serde_json::to_string(&e).unwrap();
502        assert!(json.contains("\"accountReadOnly\""));
503    }
504
505    #[test]
506    fn server_unavailable_type_string() {
507        let e = JmapError::server_unavailable();
508        let json = serde_json::to_string(&e).unwrap();
509        assert!(json.contains("\"serverUnavailable\""));
510    }
511
512    #[test]
513    fn server_fail_includes_description() {
514        let e = JmapError::server_fail("internal error");
515        let json = serde_json::to_string(&e).unwrap();
516        assert!(json.contains("\"serverFail\""));
517        assert!(json.contains("internal error"));
518    }
519
520    #[test]
521    fn server_partial_fail_type_string() {
522        let e = JmapError::server_partial_fail();
523        let json = serde_json::to_string(&e).unwrap();
524        assert!(json.contains("\"serverPartialFail\""));
525    }
526
527    #[test]
528    fn unknown_method_type_string() {
529        let e = JmapError::unknown_method();
530        let json = serde_json::to_string(&e).unwrap();
531        assert!(json.contains("\"unknownMethod\""));
532    }
533
534    #[test]
535    fn invalid_result_reference_type_string() {
536        let e = JmapError::invalid_result_reference();
537        let json = serde_json::to_string(&e).unwrap();
538        assert!(json.contains("\"invalidResultReference\""));
539    }
540
541    #[test]
542    fn cannot_calculate_changes_type_string() {
543        let e = JmapError::cannot_calculate_changes();
544        let json = serde_json::to_string(&e).unwrap();
545        assert!(json.contains("\"cannotCalculateChanges\""));
546    }
547
548    #[test]
549    fn state_mismatch_type_string() {
550        let e = JmapError::state_mismatch();
551        let json = serde_json::to_string(&e).unwrap();
552        assert!(json.contains("\"stateMismatch\""));
553    }
554
555    #[test]
556    fn too_large_type_string() {
557        let e = JmapError::too_large();
558        let json = serde_json::to_string(&e).unwrap();
559        assert!(json.contains("\"tooLarge\""));
560    }
561
562    #[test]
563    fn request_too_large_type_string() {
564        let e = JmapError::request_too_large();
565        let json = serde_json::to_string(&e).unwrap();
566        assert!(json.contains("\"requestTooLarge\""));
567        assert!(!json.contains("\"description\""));
568    }
569
570    #[test]
571    fn rate_limit_type_string() {
572        let e = JmapError::rate_limit();
573        let json = serde_json::to_string(&e).unwrap();
574        assert!(json.contains("\"rateLimit\""));
575    }
576
577    #[test]
578    fn unsupported_filter_type_string() {
579        let e = JmapError::unsupported_filter();
580        let json = serde_json::to_string(&e).unwrap();
581        assert!(json.contains("\"unsupportedFilter\""));
582    }
583
584    #[test]
585    fn anchor_not_found_type_string() {
586        let e = JmapError::anchor_not_found();
587        let json = serde_json::to_string(&e).unwrap();
588        assert!(json.contains("\"anchorNotFound\""));
589    }
590
591    #[test]
592    fn from_account_not_found_type_string() {
593        let e = JmapError::from_account_not_found();
594        let json = serde_json::to_string(&e).unwrap();
595        assert!(json.contains("\"fromAccountNotFound\""));
596        assert!(!json.contains("\"description\""));
597    }
598
599    #[test]
600    fn from_account_not_supported_by_method_type_string() {
601        let e = JmapError::from_account_not_supported_by_method();
602        let json = serde_json::to_string(&e).unwrap();
603        assert!(json.contains("\"fromAccountNotSupportedByMethod\""));
604        assert!(!json.contains("\"description\""));
605    }
606
607    #[test]
608    fn unsupported_sort_type_string() {
609        let e = JmapError::unsupported_sort();
610        let json = serde_json::to_string(&e).unwrap();
611        assert!(json.contains("\"unsupportedSort\""));
612        assert!(!json.contains("\"description\""));
613    }
614
615    #[test]
616    fn too_many_changes_with_limit_serializes_limit_field() {
617        let err = JmapError::too_many_changes_with_limit(100);
618        let v = serde_json::to_value(&err).unwrap();
619        assert_eq!(v["type"], "tooManyChanges");
620        assert_eq!(v["limit"], 100u64);
621        assert!(v.get("description").is_none());
622    }
623
624    #[test]
625    fn not_json_type_string() {
626        let e = JmapError::not_json();
627        let json = serde_json::to_string(&e).unwrap();
628        assert!(json.contains("\"notJSON\""));
629        assert!(!json.contains("\"description\""));
630    }
631
632    #[test]
633    fn not_request_type_string() {
634        let e = JmapError::not_request();
635        let json = serde_json::to_string(&e).unwrap();
636        assert!(json.contains("\"notRequest\""));
637        assert!(!json.contains("\"description\""));
638    }
639
640    #[test]
641    fn limit_carries_limit_name_in_its_own_field() {
642        // Oracle: RFC 8620 §3.6.1 — "the "limit" property MUST contain the
643        // name of the limit being applied". The name has its own field, not
644        // the human-readable description.
645        let e = JmapError::limit("maxCallsInRequest");
646        assert_eq!(e.error_type, MethodErrorType::Limit);
647        assert_eq!(e.limit_name.as_deref(), Some("maxCallsInRequest"));
648        assert_eq!(e.description, None);
649        // The name only travels in the problem-details body; the
650        // methodResponses form is just the type.
651        assert_eq!(
652            serde_json::to_value(&e).unwrap(),
653            serde_json::json!({"type": "limit"})
654        );
655    }
656
657    // Oracle: RFC 8620 §3.6.1 — unknownCapability with detail includes the URI.
658    #[test]
659    fn unknown_capability_with_detail_includes_uri() {
660        let e = JmapError::unknown_capability_with_detail("urn:example:unknown");
661        assert_eq!(e.error_type, MethodErrorType::UnknownCapability);
662        assert_eq!(e.description.as_deref(), Some("urn:example:unknown"));
663    }
664
665    #[test]
666    fn custom_error_type_round_trips() {
667        let e = JmapError::custom("urn:example:customError");
668        assert_eq!(
669            e.error_type,
670            MethodErrorType::Other("urn:example:customError".to_owned())
671        );
672        let json = serde_json::to_string(&e).unwrap();
673        assert!(json.contains("\"urn:example:customError\""));
674        let restored: JmapError = serde_json::from_str(&json).unwrap();
675        assert_eq!(restored, e);
676    }
677
678    #[test]
679    fn round_trip_deserialize() {
680        // Verify the "type" rename survives a JSON round-trip.
681        let original = JmapError::invalid_arguments("test");
682        let json = serde_json::to_string(&original).unwrap();
683        let restored: JmapError = serde_json::from_str(&json).unwrap();
684        assert_eq!(restored.error_type, MethodErrorType::InvalidArguments);
685        assert_eq!(restored.description.as_deref(), Some("test"));
686    }
687
688    // Oracle: the error type strings of RFC 8620 §3.6.1, §3.6.2 and
689    // §5.1-§5.6, hand-copied. Each must serialize to that string and decode
690    // back to its typed variant, never `Other`.
691    #[test]
692    fn method_error_type_all_known_variants_round_trip() {
693        let cases: &[(MethodErrorType, &str)] = &[
694            (MethodErrorType::UnknownCapability, "unknownCapability"),
695            (MethodErrorType::NotJson, "notJSON"),
696            (MethodErrorType::NotRequest, "notRequest"),
697            (MethodErrorType::Limit, "limit"),
698            (MethodErrorType::ServerUnavailable, "serverUnavailable"),
699            (MethodErrorType::ServerFail, "serverFail"),
700            (MethodErrorType::ServerPartialFail, "serverPartialFail"),
701            (MethodErrorType::UnknownMethod, "unknownMethod"),
702            (MethodErrorType::InvalidArguments, "invalidArguments"),
703            (
704                MethodErrorType::InvalidResultReference,
705                "invalidResultReference",
706            ),
707            (MethodErrorType::Forbidden, "forbidden"),
708            (MethodErrorType::AccountNotFound, "accountNotFound"),
709            (
710                MethodErrorType::AccountNotSupportedByMethod,
711                "accountNotSupportedByMethod",
712            ),
713            (MethodErrorType::AccountReadOnly, "accountReadOnly"),
714            (MethodErrorType::RequestTooLarge, "requestTooLarge"),
715            (
716                MethodErrorType::CannotCalculateChanges,
717                "cannotCalculateChanges",
718            ),
719            (MethodErrorType::StateMismatch, "stateMismatch"),
720            (MethodErrorType::FromAccountNotFound, "fromAccountNotFound"),
721            (
722                MethodErrorType::FromAccountNotSupportedByMethod,
723                "fromAccountNotSupportedByMethod",
724            ),
725            (MethodErrorType::AnchorNotFound, "anchorNotFound"),
726            (MethodErrorType::UnsupportedSort, "unsupportedSort"),
727            (MethodErrorType::UnsupportedFilter, "unsupportedFilter"),
728            (MethodErrorType::TooManyChanges, "tooManyChanges"),
729        ];
730        for (variant, wire) in cases {
731            assert_eq!(
732                serde_json::to_value(variant).unwrap(),
733                serde_json::Value::String((*wire).to_owned())
734            );
735            let decoded: MethodErrorType =
736                serde_json::from_value(serde_json::Value::String((*wire).to_owned())).unwrap();
737            assert_eq!(&decoded, variant, "{wire} must decode to its variant");
738            assert_eq!(&MethodErrorType::from(*wire), variant);
739        }
740        // An extension type stays Other and round-trips verbatim.
741        let decoded: MethodErrorType = serde_json::from_str(r#""rateLimited""#).unwrap();
742        assert_eq!(decoded, MethodErrorType::Other("rateLimited".to_owned()));
743        assert_eq!(serde_json::to_string(&decoded).unwrap(), r#""rateLimited""#);
744    }
745
746    // Oracle: RFC 8620 §3.4.1 fixture — methodResponses[3] is ["error", {"type":"unknownMethod"}, "c3"]
747    #[test]
748    fn fixture_response_contains_unknown_method_error() {
749        let raw = include_str!("../tests/fixtures/rfc8620-response.json");
750        let v: serde_json::Value = serde_json::from_str(raw).expect("parse fixture");
751        let inv = &v["methodResponses"][3];
752        assert_eq!(inv[0], "error");
753        assert_eq!(inv[1]["type"], "unknownMethod");
754        assert_eq!(inv[2], "c3");
755    }
756}