coap_message_utils/
error.rs

1//! Common error types
2
3#[cfg(feature = "defmt_0_3")]
4use defmt_0_3::Format;
5// This makes the feature not additive, to a hypothetical defmt_0_4, but then again, those would
6// probably conflict anyway. (If there is ever a need for multiple impls at the same time, all the
7// fallout would be that we'd have to implement Format manually, as we can only have a single defmt
8// module in scope when we derive Format)
9#[cfg(feature = "defmt_0_3")]
10use defmt_0_3 as defmt;
11
12/// A build-time-flexible renderable error type
13///
14/// This is used wherever this crate produces errors, and also recommended for outside handlers --
15/// the idea being that the more code parts share a type, the more compact code can be emitted.
16///
17/// Depending on what gets configured, it may just be a single u8 error code, or it may contain
18/// more details such as the number of the option that could not be processed, or any other
19/// Standard Problem Details (RFC9290).
20///
21/// The most typical use for this is to be used as a
22/// [`coap_handler::Handler::ExtractRequestError`](https://docs.rs/coap-handler/latest/coap_handler/trait.Handler.html#associatedtype.ExtractRequestError).
23/// It can also be used as a
24/// [`coap_handler::Handler::BuildResponseError`](https://docs.rs/coap-handler/latest/coap_handler/trait.Handler.html#associatedtype.BuildResponseError),
25/// to express late errors, but then it needs to coexist with the errors raised from writing to the
26/// response message. For that coexistence, the [`Self::from_unionerror`] conversion is provided.
27#[derive(Debug)]
28#[cfg_attr(feature = "defmt_0_3", derive(Format))]
29pub struct Error {
30    code: u8,
31    #[cfg(feature = "error_unprocessed_coap_option")]
32    unprocessed_option: Option<core::num::NonZeroU16>,
33    #[cfg(feature = "error_request_body_error_position")]
34    request_body_error_position: Option<u32>,
35    #[cfg(feature = "error_max_age")]
36    max_age: Option<u32>,
37    // reason: When not feature=error_title but debug_assertions, this field's presence in Debug is
38    // sufficient reason to have it.
39    #[allow(dead_code)]
40    #[cfg(any(feature = "error_title", debug_assertions))]
41    title: Option<&'static str>,
42}
43
44impl Error {
45    const MAX_ENCODED_LEN: usize = {
46        let mut count = 0;
47        count += 1; // up to 24 items
48        if cfg!(feature = "error_unprocessed_coap_option") {
49            // 1 item, 1+0 key, value up to 16bit
50            count += 1 + 3;
51        }
52        if cfg!(feature = "error_request_body_error_position") {
53            // 1 item, 1+1 key, value up to 64bit in theory
54            count += 2 + 5;
55        }
56        count
57    };
58
59    /// Create an error response for an unprocessed option
60    ///
61    /// The response is rendered with a single unprocessed-coap-option problem detail if that
62    /// feature is enabled.
63    ///
64    /// If the unprocessed option has a special error code (e.g. Accept => 4.06 Not Acceptable or
65    /// Content Format => 4.15 Unsupported Content-Format), that code is emitted instead of the
66    /// default 4.02 Bad Option, and the unprocessed-coap-option problem details is not emitted. In
67    /// particular, Uri-Path is turned into a 4.04 Not Found, because it indicates that some
68    /// resource did not expect any URI components after the last expected component.
69    ///
70    /// Note that the CoAP option number is given as a u16, as is common around the CoAP crates
71    /// (even though 0 is a reseved value). It is an error to pass in the value 0; the
72    /// implementation may treat this as a reason for a panic, or silently ignore the error and not
73    /// render the problem detail.
74    pub fn bad_option(unprocessed_option: u16) -> Self {
75        let special_code = match unprocessed_option {
76            coap_numbers::option::ACCEPT => Some(coap_numbers::code::NOT_ACCEPTABLE),
77            coap_numbers::option::PROXY_URI | coap_numbers::option::PROXY_SCHEME => {
78                Some(coap_numbers::code::PROXYING_NOT_SUPPORTED)
79            }
80            coap_numbers::option::CONTENT_FORMAT => {
81                Some(coap_numbers::code::UNSUPPORTED_CONTENT_FORMAT)
82            }
83            coap_numbers::option::URI_PATH => Some(coap_numbers::code::NOT_FOUND),
84            _ => None,
85        };
86
87        #[allow(unused)]
88        let unprocessed_option = if special_code.is_some() {
89            None
90        } else {
91            core::num::NonZeroU16::try_from(unprocessed_option).ok()
92        };
93
94        let code = special_code.unwrap_or(coap_numbers::code::BAD_OPTION);
95
96        #[allow(clippy::needless_update)]
97        Self {
98            code,
99            #[cfg(feature = "error_unprocessed_coap_option")]
100            unprocessed_option,
101            ..Self::otherwise_empty()
102        }
103    }
104
105    /// Create a 4.00 Bad Request error with a Request Body Error Position indicating at which
106    /// position in the request's body the error occurred
107    ///
108    /// If the crate is compiled without the `error_request_body_error_position` feature, the
109    /// position information will be ignored. The value may also be ignored if it exceeds an
110    /// internal limit of how large values can be expressed.
111    pub fn bad_request_with_rbep(#[allow(unused)] byte: usize) -> Self {
112        #[allow(clippy::needless_update)]
113        Self {
114            code: coap_numbers::code::BAD_REQUEST,
115            #[cfg(feature = "error_request_body_error_position")]
116            request_body_error_position: byte.try_into().ok(),
117            ..Self::otherwise_empty()
118        }
119    }
120
121    /// Is there any reason to even start rendering CBOR?
122    ///
123    /// This provides a precise number of items.
124    ///
125    /// Note that the presence of a `title` is *not* counted here, as the title on its own can just
126    /// as well be sent in a diagnostic payload.
127    #[inline(always)]
128    fn problem_details_count(&self) -> u8 {
129        #[allow(unused_mut)]
130        let mut count = 0;
131
132        #[cfg(feature = "error_unprocessed_coap_option")]
133        if self.unprocessed_option.is_some() {
134            count += 1;
135        }
136        #[cfg(feature = "error_request_body_error_position")]
137        if self.request_body_error_position.is_some() {
138            count += 1;
139        }
140
141        count
142    }
143
144    /// Convert [any writable message's
145    /// `UnionError`](coap_message::MinimalWritableMessage::UnionError) into an Error.
146    ///
147    /// This discards the error's details and just builds an Internal Server Error.
148    ///
149    /// The reason why this exists as a method is ergonomics: It allows handler implementations to
150    /// have a
151    /// [`BuildResponse`](https://docs.rs/coap-handler/latest/coap_handler/trait.Handler.html#tymethod.build_response)
152    /// that returns crafted [`Error`] instances, and still escalate errors from writing through
153    /// `.map_err(Error::from_unionerror)`.
154    ///
155    /// ## Downsides
156    ///
157    /// Using this might lose some debug information if the errors of the writable message turn out
158    /// to be more than Internal Server Error messages. Currently, such uses are not known; if they
159    /// come up, this method will likely be deprecated in favor of something that extracts errors
160    /// better. (One option for that point is to use an enum of Error and whatever the other thing
161    /// is; right now, this would only lead to duplication in generated machine code).
162    ///
163    /// ## Constraints
164    ///
165    /// Note that this signature does not constrain the type of `_err` a lot -- there is no hard
166    /// criterium to recognize whether a type is a conversion or write error pertaining to the
167    /// current response message, or just an arbitrary error. By convention, this is only applied
168    /// to items that can be converted into the UnionError; upholding that convention helps
169    /// spotting if ever there needs to be a replacement for this.
170    pub fn from_unionerror(
171        _err: impl core::fmt::Debug + coap_message::error::RenderableOnMinimal,
172    ) -> Self {
173        Self::internal_server_error()
174    }
175
176    /// Create an otherwise empty 4.00 Bad Request error
177    pub fn bad_request() -> Self {
178        #[allow(clippy::needless_update)]
179        Self {
180            code: coap_numbers::code::BAD_REQUEST,
181            ..Self::otherwise_empty()
182        }
183    }
184
185    /// Create an otherwise empty 4.01 Unauthorized error
186    pub fn unauthorized() -> Self {
187        #[allow(clippy::needless_update)]
188        Self {
189            code: coap_numbers::code::UNAUTHORIZED,
190            ..Self::otherwise_empty()
191        }
192    }
193
194    /// Create an otherwise empty 4.03 Forbidden error
195    pub fn forbidden() -> Self {
196        #[allow(clippy::needless_update)]
197        Self {
198            code: coap_numbers::code::FORBIDDEN,
199            ..Self::otherwise_empty()
200        }
201    }
202
203    /// Create an otherwise empty 4.04 Not Found error
204    pub fn not_found() -> Self {
205        #[allow(clippy::needless_update)]
206        Self {
207            code: coap_numbers::code::NOT_FOUND,
208            ..Self::otherwise_empty()
209        }
210    }
211
212    /// Create an otherwise empty 4.05 Method Not Allowed error
213    pub fn method_not_allowed() -> Self {
214        #[allow(clippy::needless_update)]
215        Self {
216            code: coap_numbers::code::METHOD_NOT_ALLOWED,
217            ..Self::otherwise_empty()
218        }
219    }
220
221    /// Create an otherwise empty 4.06 Not Acceptable error
222    ///
223    /// Note that this error can also be created by calling [`Self::bad_option(o)`] when `o` is an
224    /// Accept option (so there is no need for special casing by the application, but it
225    /// may be convenient to use this function when it is decided late that the requested format
226    /// was parsed turned out to be unsuitable).
227    pub fn not_acceptable() -> Self {
228        #[allow(clippy::needless_update)]
229        Self {
230            code: coap_numbers::code::NOT_ACCEPTABLE,
231            ..Self::otherwise_empty()
232        }
233    }
234
235    /// Create an otherwise empty 4.15 Unsupported Content Format error
236    ///
237    /// Note that this error can also be created by calling [`Self::bad_option(o)`] when `o` is a
238    /// Content-Format option (so there is no need for special casing by the application, but it
239    /// may be convenient to use this function when it is decided late that a content format that
240    /// was parsed turned out to be unsuitable).
241    pub fn unsupported_content_format() -> Self {
242        #[allow(clippy::needless_update)]
243        Self {
244            code: coap_numbers::code::UNSUPPORTED_CONTENT_FORMAT,
245            ..Self::otherwise_empty()
246        }
247    }
248
249    /// Create an otherwise empty 5.00 Internal Server Error error
250    pub fn internal_server_error() -> Self {
251        #[allow(clippy::needless_update)]
252        Self {
253            code: coap_numbers::code::INTERNAL_SERVER_ERROR,
254            ..Self::otherwise_empty()
255        }
256    }
257
258    /// Create an otherwise empty 5.03 Service Unavailable error
259    pub fn service_unavailable() -> Self {
260        #[allow(clippy::needless_update)]
261        Self {
262            code: coap_numbers::code::SERVICE_UNAVAILABLE,
263            ..Self::otherwise_empty()
264        }
265    }
266
267    /// Set a Max-Age
268    ///
269    /// Unlike the constructors that set error details, this modifier is only available when the
270    /// data is actually stored, because not emitting it is not just elision of possibly helful
271    /// details, but may change the networks' behavior (for example, because a long Max-Age is not
272    /// sent and the client keeps retrying every minute).
273    #[cfg(feature = "error_max_age")]
274    pub fn with_max_age(self, max_age: u32) -> Self {
275        Self {
276            max_age: Some(max_age),
277            ..self
278        }
279    }
280
281    /// Set a title on the error
282    ///
283    /// The title will be used as a diagnostic payload if no other diagnostic components are on a
284    /// message, or it will be set as the title detail. This function is available unconditionally,
285    /// but the title property is only stored if the `error_title` feature is enabled, or on debug
286    /// builds to be shown in the [Debug] representation.
287    ///
288    /// On CoAP message implementations where
289    /// [MinimalWritableMessage::promote_to_mutable_writable_message] returns None, the title may
290    /// not be shown in more complex results for lack of buffer space; where it returns Some, it
291    /// can use the full available message and is thus usually shown (unless the full error
292    /// response is too large for the message, in which case no payload is emitted).
293    #[allow(unused_variables)]
294    pub fn with_title(self, title: &'static str) -> Self {
295        Self {
296            #[cfg(any(feature = "error_title", debug_assertions))]
297            title: Some(title),
298            ..self
299        }
300    }
301
302    /// A default-ish constructor that leaves the code in an invalid state -- useful for other
303    /// constructors so they only have to cfg() out the values they need, and not every single
304    /// line.
305    ///
306    /// It is typically used as `#[allow(clippy::needless_update)] Self { code,
307    /// ..Self::otherwise_empty()}`, where the annotation quenches complaints about all members
308    /// already being set in builds with no extra features.
309    #[inline]
310    fn otherwise_empty() -> Self {
311        Self {
312            code: 0,
313            #[cfg(feature = "error_unprocessed_coap_option")]
314            unprocessed_option: None,
315            #[cfg(feature = "error_request_body_error_position")]
316            request_body_error_position: None,
317            #[cfg(feature = "error_max_age")]
318            max_age: None,
319            #[cfg(any(feature = "error_title", debug_assertions))]
320            title: None,
321        }
322    }
323}
324
325impl coap_message::error::RenderableOnMinimal for Error {
326    type Error<IE: coap_message::error::RenderableOnMinimal + core::fmt::Debug> = IE;
327
328    fn render<M: coap_message::MinimalWritableMessage>(
329        self,
330        message: &mut M,
331    ) -> Result<(), Self::Error<M::UnionError>> {
332        use coap_message::{Code, OptionNumber};
333
334        message.set_code(M::Code::new(self.code)?);
335
336        // In a minimal setup, this is unconditionally 0, and the rest of the problem details stuff
337        // should not be emitted in optimized code. If max_age is off too, the optimized function
338        // just returns here already.
339        let mut pd_count = self.problem_details_count();
340
341        if pd_count > 0 {
342            // That's quite liktely to be Some, as that registry is stable
343            const PROBLEM_DETAILS: Option<u16> =
344                coap_numbers::content_format::from_str("application/concise-problem-details+cbor");
345            // May err on stacks that can't do Content-Format (but that's rare).
346            let cfopt = M::OptionNumber::new(coap_numbers::option::CONTENT_FORMAT);
347
348            if let Some((pd, cfopt)) = PROBLEM_DETAILS.zip(cfopt.ok()) {
349                // If this goes wrong, we rather send the empty response (lest the CBOR be
350                // interpreted as plain text diagnostic payload) -- better to send the unannotated
351                // code than send even less information by falling back to an internal server error
352                // message.
353                if message.add_option_uint(cfopt, pd).is_err() {
354                    pd_count = 0;
355                }
356            }
357        };
358
359        #[cfg(feature = "error_max_age")]
360        if let Some(max_age) = self.max_age {
361            // Failure to set this is critical in the sense that we better report it as an internal
362            // server error. If problem details are involved, the server should remove that in its
363            // error path.
364            message.add_option_uint(
365                M::OptionNumber::new(coap_numbers::option::MAX_AGE)?,
366                max_age,
367            )?;
368        }
369
370        let encode = |mut cursor: minicbor::encode::write::Cursor<_>, try_include_title| {
371            let mut encoder = minicbor::Encoder::new(&mut cursor);
372
373            #[cfg(feature = "error_title")]
374            let extra_length_for_title = u64::from(try_include_title && self.title.is_some());
375            #[cfg(not(feature = "error_title"))]
376            let extra_length_for_title = 0;
377            #[cfg(not(feature = "error_title"))]
378            let _ = try_include_title;
379
380            #[allow(unused_mut)]
381            let mut encoder = encoder.map(u64::from(pd_count) + extra_length_for_title)?;
382
383            #[cfg(feature = "error_unprocessed_coap_option")]
384            if let Some(unprocessed_option) = self.unprocessed_option {
385                encoder = encoder.i8(-8)?;
386                encoder = encoder.u16(unprocessed_option.into())?;
387            }
388            #[cfg(feature = "error_request_body_error_position")]
389            if let Some(position) = self.request_body_error_position {
390                encoder = encoder.i8(-25)?;
391                encoder = encoder.u32(position)?;
392            }
393            #[cfg(feature = "error_title")]
394            if try_include_title {
395                if let Some(title) = self.title {
396                    encoder = encoder.i8(-1)?;
397                    encoder = encoder.str(title)?;
398                }
399            }
400            let _ = encoder;
401            let written = cursor.position();
402            Ok(written)
403        };
404
405        if pd_count > 0 {
406            if let Some(message) = message.promote_to_mutable_writable_message() {
407                use coap_message::MutableWritableMessage;
408                let max_len = Self::MAX_ENCODED_LEN;
409                #[cfg(feature = "error_title")]
410                let max_len = max_len
411                    + match self.title {
412                        Some(t) => 1 + 5 + t.len(),
413                        None => 0,
414                    };
415                let payload = message.payload_mut_with_len(max_len)?;
416                let cursor = minicbor::encode::write::Cursor::new(payload);
417
418                if let Ok::<_, minicbor::encode::Error<_>>(written) = encode(cursor, true) {
419                    message.truncate(written)?;
420                } else {
421                    message.truncate(0)?;
422                }
423            } else {
424                // When monomorphized for a message type where promote_to_mutable_writable_message
425                // is inline Some, this branch should not even be emitted, let alone taken.
426
427                let mut buf = [0u8; Self::MAX_ENCODED_LEN];
428                let cursor = minicbor::encode::write::Cursor::new(buf.as_mut());
429                if let Ok::<_, minicbor::encode::Error<_>>(written) = encode(cursor, false) {
430                    message.set_payload(&buf[..written])?;
431                }
432            }
433        } else {
434            #[cfg(feature = "error_title")]
435            if let Some(title) = self.title {
436                message.set_payload(title.as_bytes())?;
437            }
438        }
439
440        Ok(())
441    }
442}