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}