Line data Source code
1 : // SPDX-License-Identifier: EUPL-1.2
2 : //! NGSI-LD error model: the Table 5.5.2-1 error-type vocabulary (variant
3 : //! names verbatim) with the Table 6.3.2-1 HTTP status mapping. Error type
4 : //! URI base is https (V1.9.1). errors/Conflict entered with 5.9.2.4
5 : //! (registration-vs-entity/registration proxied-mode conflicts, 409).
6 :
7 : use serde::Serialize;
8 : use thiserror::Error;
9 :
10 : /// Base of every NGSI-LD error type URI (Table 5.5.2-1, https in V1.9.1).
11 : pub const ERROR_TYPE_BASE: &str = "https://uri.etsi.org/ngsi-ld/errors/";
12 :
13 : /// NGSI-LD error types of Table 5.5.2-1; the payload is the `detail` text.
14 : #[derive(Debug, Error)]
15 : pub enum NgsiError {
16 : /// The referred element already exists (409).
17 : #[error("{0}")]
18 : AlreadyExists(String),
19 : /// The request or its content is incorrect (400).
20 : #[error("{0}")]
21 : BadRequestData(String),
22 : /// Registration-vs-entity or proxied-mode registration conflict (409).
23 : #[error("{0}")]
24 : Conflict(String),
25 : /// The request is not valid (400).
26 : #[error("{0}")]
27 : InvalidRequest(String),
28 : /// An unexpected internal error (500).
29 : #[error("{0}")]
30 : InternalError(String),
31 : /// A remote JSON-LD @context could not be retrieved (504).
32 : #[error("{0}")]
33 : LdContextNotAvailable(String),
34 : /// Multi-tenancy is not supported by this broker (501).
35 : #[error("{0}")]
36 : NoMultiTenantSupport(String),
37 : /// The tenant named in `NGSILD-Tenant` does not exist (404).
38 : #[error("{0}")]
39 : NonexistentTenant(String),
40 : /// The operation is not supported (422).
41 : #[error("{0}")]
42 : OperationNotSupported(String),
43 : /// The referred resource has not been found (404).
44 : #[error("{0}")]
45 : ResourceNotFound(String),
46 : /// The query is too complex to be processed (403).
47 : #[error("{0}")]
48 : TooComplexQuery(String),
49 : /// The query would return too many results (403).
50 : #[error("{0}")]
51 : TooManyResults(String),
52 : }
53 :
54 : /// The `InternalError` detail a storage driver sets when its connection
55 : /// pool ran out of time handing over a connection. It is not a Table
56 : /// 6.3.2-1 error type: the HTTP binding answers it 503 with `Retry-After`
57 : /// (6.3.2 "implementations shall support the standard specific errors of
58 : /// HTTP bindings, such as the following", an open list). Both the driver
59 : /// that raises it and the binding that recognises it name this constant,
60 : /// so the two ends cannot drift.
61 : pub const DB_OVERLOADED: &str = "database overloaded";
62 :
63 : impl NgsiError {
64 : /// HTTP status per Table 6.3.2-1.
65 13387 : pub fn status(&self) -> u16 {
66 13387 : match self {
67 1050 : Self::AlreadyExists(_) | Self::Conflict(_) => 409,
68 2512 : Self::BadRequestData(_) | Self::InvalidRequest(_) => 400,
69 32 : Self::InternalError(_) => 500,
70 : // 6.3.2 Table 6.3.2-1 (V1.9.1): LdContextNotAvailable → 504.
71 : // The suite's V1.8-era 503 expectations are fixed in the
72 : // local suite fork.
73 4102 : Self::LdContextNotAvailable(_) => 504,
74 4 : Self::NoMultiTenantSupport(_) => 501,
75 5650 : Self::NonexistentTenant(_) | Self::ResourceNotFound(_) => 404,
76 12 : Self::OperationNotSupported(_) => 422,
77 25 : Self::TooComplexQuery(_) | Self::TooManyResults(_) => 403,
78 : }
79 13387 : }
80 :
81 : /// Spec error name == variant name.
82 26608 : pub fn kind(&self) -> &'static str {
83 26608 : match self {
84 2040 : Self::AlreadyExists(_) => "AlreadyExists",
85 4508 : Self::BadRequestData(_) => "BadRequestData",
86 54 : Self::Conflict(_) => "Conflict",
87 378 : Self::InvalidRequest(_) => "InvalidRequest",
88 62 : Self::InternalError(_) => "InternalError",
89 8202 : Self::LdContextNotAvailable(_) => "LdContextNotAvailable",
90 6 : Self::NoMultiTenantSupport(_) => "NoMultiTenantSupport",
91 122 : Self::NonexistentTenant(_) => "NonexistentTenant",
92 20 : Self::OperationNotSupported(_) => "OperationNotSupported",
93 11174 : Self::ResourceNotFound(_) => "ResourceNotFound",
94 6 : Self::TooComplexQuery(_) => "TooComplexQuery",
95 36 : Self::TooManyResults(_) => "TooManyResults",
96 : }
97 26608 : }
98 :
99 : /// Renders this error as the RFC 7807 body of 6.3.6.
100 13254 : pub fn to_problem_details(&self) -> ProblemDetails {
101 13254 : ProblemDetails {
102 13254 : r#type: format!("{ERROR_TYPE_BASE}{}", self.kind()),
103 13254 : title: self.kind().to_owned(),
104 13254 : status: self.status(),
105 13254 : detail: self.to_string(),
106 13254 : }
107 13254 : }
108 : }
109 :
110 : /// RFC 7807 body (always `application/json`, fully-qualified names — 6.3.6).
111 : #[derive(Debug, Serialize)]
112 : pub struct ProblemDetails {
113 : /// Error type URI: `ERROR_TYPE_BASE` + error name.
114 : pub r#type: String,
115 : /// Short summary — the error name.
116 : pub title: String,
117 : /// HTTP status per Table 6.3.2-1.
118 : pub status: u16,
119 : /// Human-readable explanation of this occurrence.
120 : pub detail: String,
121 : }
122 :
123 : #[cfg(test)]
124 : mod tests {
125 : use super::*;
126 :
127 : /// 6.3.2 Table 6.3.2-1 — every row of the error-type → HTTP status
128 : /// mapping (V1.9.1, PDF p.269), plus the project's Conflict extension.
129 : #[test]
130 2 : fn status_mapping_matches_table_6_3_2_1() {
131 2 : assert_eq!(NgsiError::AlreadyExists(String::new()).status(), 409);
132 2 : assert_eq!(NgsiError::BadRequestData(String::new()).status(), 400);
133 2 : assert_eq!(NgsiError::InternalError(String::new()).status(), 500);
134 2 : assert_eq!(NgsiError::InvalidRequest(String::new()).status(), 400);
135 2 : assert_eq!(
136 2 : NgsiError::LdContextNotAvailable(String::new()).status(),
137 : 504
138 : );
139 2 : assert_eq!(NgsiError::NoMultiTenantSupport(String::new()).status(), 501);
140 2 : assert_eq!(NgsiError::NonexistentTenant(String::new()).status(), 404);
141 2 : assert_eq!(
142 2 : NgsiError::OperationNotSupported(String::new()).status(),
143 : 422
144 : );
145 2 : assert_eq!(NgsiError::ResourceNotFound(String::new()).status(), 404);
146 2 : assert_eq!(NgsiError::TooComplexQuery(String::new()).status(), 403);
147 2 : assert_eq!(NgsiError::TooManyResults(String::new()).status(), 403);
148 2 : assert_eq!(NgsiError::Conflict(String::new()).status(), 409);
149 2 : }
150 :
151 : /// 5.5.2 Table 5.5.2-1 — the error names are the wire contract: clients
152 : /// branch on the type URI, so every variant's name is pinned here, not
153 : /// just the one the round-trip test happens to use.
154 : #[test]
155 2 : fn every_error_type_uri_matches_table_5_5_2_1() {
156 24 : for (e, name) in [
157 2 : (NgsiError::AlreadyExists(String::new()), "AlreadyExists"),
158 2 : (NgsiError::BadRequestData(String::new()), "BadRequestData"),
159 2 : (NgsiError::Conflict(String::new()), "Conflict"),
160 2 : (NgsiError::InternalError(String::new()), "InternalError"),
161 2 : (NgsiError::InvalidRequest(String::new()), "InvalidRequest"),
162 2 : (
163 2 : NgsiError::LdContextNotAvailable(String::new()),
164 2 : "LdContextNotAvailable",
165 2 : ),
166 2 : (
167 2 : NgsiError::NoMultiTenantSupport(String::new()),
168 2 : "NoMultiTenantSupport",
169 2 : ),
170 2 : (
171 2 : NgsiError::NonexistentTenant(String::new()),
172 2 : "NonexistentTenant",
173 2 : ),
174 2 : (
175 2 : NgsiError::OperationNotSupported(String::new()),
176 2 : "OperationNotSupported",
177 2 : ),
178 2 : (
179 2 : NgsiError::ResourceNotFound(String::new()),
180 2 : "ResourceNotFound",
181 2 : ),
182 2 : (NgsiError::TooComplexQuery(String::new()), "TooComplexQuery"),
183 2 : (NgsiError::TooManyResults(String::new()), "TooManyResults"),
184 2 : ] {
185 24 : assert_eq!(e.kind(), name);
186 24 : let pd = e.to_problem_details();
187 24 : assert_eq!(pd.r#type, format!("{ERROR_TYPE_BASE}{name}"));
188 24 : assert_eq!(pd.title, name);
189 24 : assert!(
190 24 : !pd.r#type.starts_with("http://"),
191 : "the V1.9.1 base is https"
192 : );
193 : }
194 2 : }
195 :
196 : /// 6.3.6 / RFC 7807: the member names are `type`, `title`, `status` and
197 : /// `detail` — the Rust raw identifier must not leak as `r#type`.
198 : #[test]
199 2 : fn problem_details_serializes_rfc_7807_member_names() {
200 2 : let body =
201 2 : serde_json::to_value(NgsiError::TooManyResults("too many".into()).to_problem_details())
202 2 : .expect("serialize");
203 2 : let obj = body.as_object().expect("object");
204 2 : let mut keys: Vec<&str> = obj.keys().map(String::as_str).collect();
205 2 : keys.sort_unstable();
206 2 : assert_eq!(keys, ["detail", "status", "title", "type"]);
207 2 : assert_eq!(obj["status"], 403);
208 2 : }
209 :
210 : #[test]
211 2 : fn problem_details_uses_https_base() {
212 2 : let pd = NgsiError::ResourceNotFound("nope".into()).to_problem_details();
213 2 : assert_eq!(
214 : pd.r#type,
215 : "https://uri.etsi.org/ngsi-ld/errors/ResourceNotFound"
216 : );
217 2 : assert_eq!(pd.status, 404);
218 2 : }
219 : }
|