1use std::error::Error;
2use std::fmt::{self, Display, Formatter};
3
4use http::StatusCode;
5use serde_json::{Map, Value as Json};
6use wip_protocol::{ProtocolError, ProtocolErrorCode, ProtocolInteraction};
7
8use crate::{BodyKind, CodecError, Limits, json};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum DecodedResponse<T> {
13 Success(T),
15 ProtocolFailure {
17 error: ProtocolError,
19 status_mismatch: Option<StatusMismatch>,
21 },
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct StatusMismatch {
27 pub actual: StatusCode,
29 pub expected: StatusCode,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum TransportFailureKind {
36 AuthenticationRequired,
38 EndpointForbidden,
40 UnsupportedContentCoding,
42 HttpStatus,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct TransportBindingFailure {
49 pub status: StatusCode,
51 pub kind: TransportFailureKind,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum InvalidResponseKind {
58 ErrorEnvelopeOnSuccessStatus,
60 SuccessBodyOnErrorStatus,
62 DisallowedProtocolCode,
64 NonCanonicalSuccessStatus,
66 InvalidSuccessBody,
68}
69
70#[derive(Debug)]
72pub enum ClientResponseError {
73 TransportBinding(TransportBindingFailure),
75 InvalidResponse {
77 kind: InvalidResponseKind,
79 detail: String,
81 },
82 UnsupportedDescriptorFormat {
84 format: String,
86 },
87}
88
89impl ClientResponseError {
90 pub(crate) fn invalid(kind: InvalidResponseKind, detail: impl Into<String>) -> Self {
91 Self::InvalidResponse {
92 kind,
93 detail: detail.into(),
94 }
95 }
96}
97
98impl Display for ClientResponseError {
99 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
100 match self {
101 Self::TransportBinding(failure) => write!(
102 formatter,
103 "transport binding failure ({:?}, HTTP {})",
104 failure.kind, failure.status
105 ),
106 Self::InvalidResponse { kind, detail } => {
107 write!(formatter, "invalid WIP response ({kind:?}): {detail}")
108 }
109 Self::UnsupportedDescriptorFormat { format } => {
110 write!(
111 formatter,
112 "unsupported interface descriptor format `{format}`"
113 )
114 }
115 }
116 }
117}
118
119impl Error for ClientResponseError {}
120
121#[must_use]
123pub const fn status_for_error(code: ProtocolErrorCode) -> StatusCode {
124 match code {
125 ProtocolErrorCode::InvalidRequest => StatusCode::BAD_REQUEST,
126 ProtocolErrorCode::NotFound | ProtocolErrorCode::InterfaceNotFound => StatusCode::NOT_FOUND,
127 ProtocolErrorCode::InterfaceMismatch => StatusCode::CONFLICT,
128 ProtocolErrorCode::OperationNotFound => StatusCode::NOT_FOUND,
129 ProtocolErrorCode::InvalidArguments => StatusCode::UNPROCESSABLE_ENTITY,
130 ProtocolErrorCode::ValidatorRequired | ProtocolErrorCode::InterfaceValidatorRequired => {
131 StatusCode::PRECONDITION_REQUIRED
132 }
133 ProtocolErrorCode::ValidatorMismatch | ProtocolErrorCode::InterfaceValidatorMismatch => {
134 StatusCode::PRECONDITION_FAILED
135 }
136 ProtocolErrorCode::PermissionDenied => StatusCode::FORBIDDEN,
137 ProtocolErrorCode::ResourceLimitExceeded => StatusCode::UNPROCESSABLE_ENTITY,
138 ProtocolErrorCode::Internal | ProtocolErrorCode::OperationOutcomeUnknown => {
139 StatusCode::INTERNAL_SERVER_ERROR
140 }
141 }
142}
143
144fn status_matches_error(status: StatusCode, code: ProtocolErrorCode) -> bool {
145 status == status_for_error(code)
146}
147
148#[must_use]
150pub const fn error_code_name(code: ProtocolErrorCode) -> &'static str {
151 match code {
152 ProtocolErrorCode::InvalidRequest => "invalid_request",
153 ProtocolErrorCode::NotFound => "not_found",
154 ProtocolErrorCode::InterfaceNotFound => "interface_not_found",
155 ProtocolErrorCode::InterfaceMismatch => "interface_mismatch",
156 ProtocolErrorCode::OperationNotFound => "operation_not_found",
157 ProtocolErrorCode::InvalidArguments => "invalid_arguments",
158 ProtocolErrorCode::ValidatorRequired => "validator_required",
159 ProtocolErrorCode::ValidatorMismatch => "validator_mismatch",
160 ProtocolErrorCode::InterfaceValidatorRequired => "interface_validator_required",
161 ProtocolErrorCode::InterfaceValidatorMismatch => "interface_validator_mismatch",
162 ProtocolErrorCode::PermissionDenied => "permission_denied",
163 ProtocolErrorCode::ResourceLimitExceeded => "resource_limit_exceeded",
164 ProtocolErrorCode::Internal => "internal",
165 ProtocolErrorCode::OperationOutcomeUnknown => "operation_outcome_unknown",
166 }
167}
168
169fn error_code_from_name(name: &str) -> Option<ProtocolErrorCode> {
170 Some(match name {
171 "invalid_request" => ProtocolErrorCode::InvalidRequest,
172 "not_found" => ProtocolErrorCode::NotFound,
173 "interface_not_found" => ProtocolErrorCode::InterfaceNotFound,
174 "interface_mismatch" => ProtocolErrorCode::InterfaceMismatch,
175 "operation_not_found" => ProtocolErrorCode::OperationNotFound,
176 "invalid_arguments" => ProtocolErrorCode::InvalidArguments,
177 "validator_required" => ProtocolErrorCode::ValidatorRequired,
178 "validator_mismatch" => ProtocolErrorCode::ValidatorMismatch,
179 "interface_validator_required" => ProtocolErrorCode::InterfaceValidatorRequired,
180 "interface_validator_mismatch" => ProtocolErrorCode::InterfaceValidatorMismatch,
181 "permission_denied" => ProtocolErrorCode::PermissionDenied,
182 "resource_limit_exceeded" => ProtocolErrorCode::ResourceLimitExceeded,
183 "internal" => ProtocolErrorCode::Internal,
184 "operation_outcome_unknown" => ProtocolErrorCode::OperationOutcomeUnknown,
185 _ => return None,
186 })
187}
188
189pub(crate) fn error_to_json(error: &ProtocolError) -> Json {
190 serde_json::json!({
191 "error": {
192 "code": error_code_name(error.code),
193 "message": error.message,
194 }
195 })
196}
197
198fn protocol_error_from_json(value: &Json) -> Result<ProtocolError, CodecError> {
199 let fields = value
200 .as_object()
201 .ok_or_else(|| invalid_field("error", "expected object"))?;
202 exact(fields, &["code", "message"], "error")?;
203 let code_name = required_string(fields, "code")?;
204 let code = error_code_from_name(code_name)
205 .ok_or_else(|| invalid_field("error.code", "unknown protocol error code"))?;
206 let message = required_string(fields, "message")?.to_owned();
207 Ok(ProtocolError { code, message })
208}
209
210fn envelope_error_from_json(value: &Json) -> Result<Option<ProtocolError>, CodecError> {
211 let Some(fields) = value.as_object() else {
212 return Ok(None);
213 };
214 if fields.len() != 1 || !fields.contains_key("error") {
215 return Ok(None);
216 }
217 protocol_error_from_json(&fields["error"]).map(Some)
218}
219
220fn exact(fields: &Map<String, Json>, allowed: &[&str], parent: &str) -> Result<(), CodecError> {
221 for name in fields.keys() {
222 if !allowed.contains(&name.as_str()) {
223 return Err(CodecError::UnknownField {
224 field: format!("{parent}.{name}"),
225 });
226 }
227 }
228 for name in allowed {
229 if !fields.contains_key(*name) {
230 return Err(CodecError::MissingField {
231 field: format!("{parent}.{name}"),
232 });
233 }
234 }
235 Ok(())
236}
237
238fn required_string<'a>(fields: &'a Map<String, Json>, name: &str) -> Result<&'a str, CodecError> {
239 fields
240 .get(name)
241 .and_then(Json::as_str)
242 .ok_or_else(|| invalid_field(&format!("error.{name}"), "expected string"))
243}
244
245fn invalid_field(field: &str, reason: &str) -> CodecError {
246 CodecError::InvalidField {
247 field: field.into(),
248 reason: reason.into(),
249 }
250}
251
252pub(crate) fn decode<T>(
253 status: StatusCode,
254 body: &[u8],
255 interaction: ProtocolInteraction,
256 limits: Limits,
257 success: impl FnOnce(&Json) -> Result<T, CodecError>,
258) -> Result<DecodedResponse<T>, ClientResponseError> {
259 let value = match json::parse(body, BodyKind::Response, limits) {
260 Ok(value) => value,
261 Err(_) if !status.is_success() => {
262 return Err(transport_failure(status));
263 }
264 Err(error) => {
265 return Err(ClientResponseError::invalid(
266 InvalidResponseKind::InvalidSuccessBody,
267 error.to_string(),
268 ));
269 }
270 };
271
272 match envelope_error_from_json(&value) {
273 Ok(Some(error)) => {
274 if status.is_success() {
275 return Err(ClientResponseError::invalid(
276 InvalidResponseKind::ErrorEnvelopeOnSuccessStatus,
277 "protocol error envelope carried by a success status",
278 ));
279 }
280 if !error.code.is_allowed_for(interaction) {
281 return Err(ClientResponseError::invalid(
282 InvalidResponseKind::DisallowedProtocolCode,
283 format!(
284 "code `{}` is not allowed for {interaction:?}",
285 error_code_name(error.code)
286 ),
287 ));
288 }
289 let expected = status_for_error(error.code);
290 let status_mismatch =
291 (!status_matches_error(status, error.code)).then_some(StatusMismatch {
292 actual: status,
293 expected,
294 });
295 Ok(DecodedResponse::ProtocolFailure {
296 error,
297 status_mismatch,
298 })
299 }
300 Err(_) if !status.is_success() => Err(transport_failure(status)),
301 Err(error) => Err(ClientResponseError::invalid(
302 InvalidResponseKind::InvalidSuccessBody,
303 error.to_string(),
304 )),
305 Ok(None) if status == StatusCode::OK => success(&value)
306 .map(DecodedResponse::Success)
307 .map_err(|error| {
308 ClientResponseError::invalid(
309 InvalidResponseKind::InvalidSuccessBody,
310 error.to_string(),
311 )
312 }),
313 Ok(None) if status.is_success() => match success(&value) {
314 Ok(_) => Err(ClientResponseError::invalid(
315 InvalidResponseKind::NonCanonicalSuccessStatus,
316 format!("success body carried by non-canonical HTTP {status}"),
317 )),
318 Err(error) => Err(ClientResponseError::invalid(
319 InvalidResponseKind::InvalidSuccessBody,
320 error.to_string(),
321 )),
322 },
323 Ok(None) => match success(&value) {
324 Ok(_) => Err(ClientResponseError::invalid(
325 InvalidResponseKind::SuccessBodyOnErrorStatus,
326 "valid success body carried by a non-success status",
327 )),
328 Err(_) => Err(transport_failure(status)),
329 },
330 }
331}
332
333pub(crate) fn unsupported_content_coding(status: StatusCode) -> ClientResponseError {
334 ClientResponseError::TransportBinding(TransportBindingFailure {
335 status,
336 kind: TransportFailureKind::UnsupportedContentCoding,
337 })
338}
339
340pub(crate) fn transport_failure(status: StatusCode) -> ClientResponseError {
341 let kind = match status {
342 StatusCode::UNAUTHORIZED => TransportFailureKind::AuthenticationRequired,
343 StatusCode::FORBIDDEN => TransportFailureKind::EndpointForbidden,
344 _ => TransportFailureKind::HttpStatus,
345 };
346 ClientResponseError::TransportBinding(TransportBindingFailure { status, kind })
347}