1- //! The structured fields an error carries, and what an error may contain.
1+ //! Inspect error codes, recovery hints, and structured details.
2+ //!
3+ //! [`ProfileError`](crate::ProfileError) provides a readable message through
4+ //! `Display`, a code and optional help through [`miette::Diagnostic`], and
5+ //! structured fields through [`ErrorPayload::payload`]. Use these fields when
6+ //! you need details such as a missing profile's path or a JSON error's line
7+ //! and column. You do not need to parse the message.
8+ //!
9+ //! # Read an error's details
10+ //!
11+ //! Import both traits to access their methods:
12+ //!
13+ //! ```
14+ //! use miette::Diagnostic;
15+ //! use stack_profile::{ErrorPayload, ProfileError};
16+ //!
17+ //! let error = ProfileError::NotFound {
18+ //! path: "auth.json".into(),
19+ //! };
20+ //!
21+ //! assert_eq!(error.code().unwrap().to_string(), "stack_profile::not_found");
22+ //! assert_eq!(error.payload()["path"], "auth.json");
23+ //! assert!(error.help().is_some());
24+ //! ```
25+ //!
26+ //! In Rust, match error variants to decide how to handle a failure. Codes
27+ //! identify errors when reporting them to another language or service; fields
28+ //! provide the details needed to explain or handle that failure. Payload keys
29+ //! use `snake_case`, and errors without additional details return an empty map.
30+ //!
31+ //! `stack-auth`, `stack-kms`, and `stack-encrypt` use the same trait and
32+ //! re-export it, so you can import it from the crate you already use.
33+ //!
34+ //! # Handle underlying errors with care
35+ //!
36+ //! These crates avoid including sensitive input in diagnostic messages, help,
37+ //! and payloads. Wrapped library errors remain available through
38+ //! [`std::error::Error::source`] for local troubleshooting. Their messages may
39+ //! contain input, credentials, or URLs: inspect them before including them in
40+ //! logs or reports. Do not assume that formatting an entire error chain is
41+ //! safe because its top-level message omits sensitive values.
42+ //!
43+ //! For example, [`describe_json_error`] reports the error kind, line, and
44+ //! column instead of the JSON parser's message, which can quote profile
45+ //! contents containing a token.
46+ //!
47+ //! # Implement an error payload
48+ //!
49+ //! Implement [`ErrorPayload`] for an error that already implements
50+ //! [`miette::Diagnostic`]. Override `payload()` to return its structured
51+ //! details, using the [`payload`] helper to build a map from name/value pairs.
52+ //! Keep the default empty map when there are no useful details.
53+ //!
54+ //! The following policy applies to messages, help, payload fields, and any
55+ //! causes reported through a language binding.
56+ //!
57+ //! ## Allowed details
58+ //!
59+ //! - Keyset IDs and names; counts and lengths; index names and kinds.
60+ //! - Schema field names from a plan or Go struct, rather than field values.
61+ //! - Request kinds and HTTP status codes for ZeroKMS, CipherStash's
62+ //! key-management service.
63+ //! - Workspace IDs, workspace resource names (CRNs), and regions.
64+ //! - Profile file paths, which identify a store rather than its contents.
65+ //! - The CipherStash token service's OAuth `error_description`, intended for
66+ //! people to read.
67+ //! - Messages from errors governed by this policy, or fixed library messages
68+ //! such as those from `url::ParseError`.
69+ //!
70+ //! ## Details to exclude
71+ //!
72+ //! Never include plaintext, key material (data, index, or client keys), access
73+ //! or refresh tokens, ciphertext bytes, search-index bytes, or raw context
74+ //! values. Contexts can contain customer data taken from record fields; report
75+ //! their length and number of parts instead of their contents.
76+ //!
77+ //! Do not forward arbitrary library error messages. Report the operation or
78+ //! error kind instead, and retain the original error through `source()`.
79+ //! Likewise, omit ZeroKMS response bodies because they may echo request data;
80+ //! report the request kind and HTTP status instead.
81+ //!
82+ //! Custom implementations that supply boxed errors, such as
83+ //! `stack_encrypt::Error::Other` or `dynamic::TargetError::Other`, are
84+ //! responsible for their messages: these messages are displayed as supplied.
85+ //! Describe what failed without quoting the value or ciphertext, and do not
86+ //! pass through another library's message.
87+ //!
88+ //! If a new field is not covered by this policy, decide whether it is safe
89+ //! and document that decision here before adding it.
290
3- /// The structured fields an error carries beside its message, its miette
4- /// [`code`](miette::Diagnostic::code) and its help.
91+ /// Structured error details that callers can read without parsing a message.
592///
6- /// Every error in `stack-profile`, `stack-auth`, `stack-kms` and
7- /// `stack-encrypt` implements it. Those are the crates whose errors reach a
8- /// caller through a language binding. The trait is defined here because this
9- /// crate is the one all four depend on, and each of the others re-exports
10- /// it, so a caller names it from the crate it uses.
11- ///
12- /// [`payload`](Self::payload) gives the error's facts as data: which keyset a
13- /// value was sealed under, which plan field was refused, how long a context
14- /// was. A binding hands them to its caller beside the code, so the caller can
15- /// branch on a field instead of parsing a message. Codes are for crossing a
16- /// boundary: Rust code that needs to branch on an error matches the variant.
17- ///
18- /// # What an error may contain
19- ///
20- /// The rule covers an error's message (its `Display`), its help and every
21- /// field of its payload, and every error under it that a binding reports as
22- /// a cause. The Go guests enforce it with a leak test: every error path a
23- /// test can reach is driven with marker values, and the test fails if a
24- /// marker appears anywhere in what the guest encodes.
25- ///
26- /// **Allowed:**
27- ///
28- /// - keyset ids and names
29- /// - counts and lengths
30- /// - term kinds and index names
31- /// - field names from a plan or a Go struct: these describe the schema, not
32- /// the data
33- /// - ZeroKMS request kinds and HTTP status numbers
34- /// - workspace ids, workspace CRNs and region names
35- /// - the path of a profile file, which names the store and not its contents
36- /// - a description the CipherStash token service sends for a person to read
37- /// (an OAuth `error_description`)
38- /// - the message of an error this rule also governs: one from these four
39- /// crates, or from a library whose messages are fixed text, such as
40- /// `url::ParseError`
41- ///
42- /// **Never allowed:**
43- ///
44- /// - plaintext, or any part of it
45- /// - key material: data keys, index keys, client keys
46- /// - access tokens and refresh tokens
47- /// - ciphertext bytes and index term bytes
48- /// - raw context values
49- ///
50- /// **Decided, with the reason:**
51- ///
52- /// - **Context descriptors are left out.** A context can be built from a
53- /// record field (`#[stash(context_field)]`), so its descriptor can hold
54- /// customer data. An error about a context gives the descriptor's length
55- /// and its number of parts instead.
56- /// - **An error from another library gives its type, not its message.** That
57- /// message is text these crates do not control: an HTTP client's error can
58- /// carry a URL with its query string, and a JSON parser's can quote the
59- /// input it refused. Where these crates wrap such an error, their message
60- /// names what failed, and the wrapped error stays reachable through
61- /// [`source`](std::error::Error::source) for a caller in the same process,
62- /// who decides what to log. It never appears in a message or a payload.
63- /// - **A slot any implementation can fill shows that implementation's
64- /// message**, and the implementation answers for it under this rule. Such
65- /// a slot is a `Box<dyn Error>` a trait implementor hands back:
66- /// `stack_encrypt::Error::Other` from an `EncryptFrom` or `DecryptInto`
67- /// implementation, `TargetError::Other` from an EQL type resolver. Their
68- /// own report ("unsupported EQL ciphertext producer or version") is the
69- /// one a caller needs, so it is shown as given. An implementation that
70- /// fills one names what it refused, never a byte of the value or the
71- /// ciphertext, and does not pass another library's message through.
72- /// - **ZeroKMS response bodies are left out**, until someone confirms that a
73- /// ZeroKMS error body never echoes what the request carried. An error from
74- /// a ZeroKMS request gives the request kind and the HTTP status instead.
75- ///
76- /// A field that falls under none of these needs a decision before it is
77- /// added, recorded here.
93+ /// Use [`miette::Diagnostic`] for the code and help, and [`payload`](Self::payload)
94+ /// for fields such as a profile path or a JSON error's position. See the
95+ /// [module documentation](crate::diagnostic) for examples and the policy on error contents.
7896pub trait ErrorPayload : miette:: Diagnostic {
79- /// The error's structured fields, keyed by `snake_case` name. Every value
80- /// obeys the rule above. Empty unless the error has facts worth
81- /// branching on.
97+ /// Returns structured details with `snake_case` keys.
98+ ///
99+ /// Values must follow this module's policy on error contents. The default
100+ /// implementation returns an empty map.
82101 fn payload ( & self ) -> serde_json:: Map < String , serde_json:: Value > {
83102 serde_json:: Map :: new ( )
84103 }
85104}
86105
87- /// A payload built from `(name, value)` pairs.
106+ /// Builds a payload map from `(name, value)` pairs.
88107///
89- /// Shared by the four crates' [`ErrorPayload`] impls so each reads as a list
90- /// of fields rather than a map built by hand .
108+ /// Use this when implementing [`ErrorPayload::payload`]. Names should use
109+ /// `snake_case`, and values must follow this module's policy on error contents .
91110pub fn payload < const N : usize > (
92111 fields : [ ( & str , serde_json:: Value ) ; N ] ,
93112) -> serde_json:: Map < String , serde_json:: Value > {
@@ -97,9 +116,11 @@ pub fn payload<const N: usize>(
97116 . collect ( )
98117}
99118
100- /// A JSON error as the rule allows it: what kind of error, and where. Never
101- /// serde_json's own message, which can quote the input it refused:
102- /// `syntax error at line 1 column 5`.
119+ /// Describes a JSON error by kind, line, and column without quoting input.
120+ ///
121+ /// For example: `syntax error at line 1 column 5`. Use this instead of the
122+ /// parser's own message when the input may contain credentials or other
123+ /// sensitive data.
103124pub fn describe_json_error ( error : & serde_json:: Error ) -> String {
104125 let kind = match error. classify ( ) {
105126 serde_json:: error:: Category :: Io => "read error" ,
@@ -110,12 +131,11 @@ pub fn describe_json_error(error: &serde_json::Error) -> String {
110131 format ! ( "{kind} at line {} column {}" , error. line( ) , error. column( ) )
111132}
112133
113- /// Whether `code` has the shape every code from these crates has: the crate's
114- /// name, `::`, then a `snake_case` name — `stack_encrypt::foreign_keyset`.
134+ /// Checks that a code uses the given crate prefix and a `snake_case` name.
115135///
116- /// Each crate's code test runs every code it can produce through this, so a
117- /// code in the wrong crate's namespace, or spelled in another case, fails
118- /// there rather than reaching a binding .
136+ /// For example, `is_code_of("stack_profile", "stack_profile::not_found")`
137+ /// returns `true`. This checks the format, not whether the crate defines the
138+ /// code; use the crate's `ERROR_CODES` list to check membership .
119139pub fn is_code_of ( crate_name : & str , code : & str ) -> bool {
120140 let Some ( name) = code
121141 . strip_prefix ( crate_name)
0 commit comments