aditjind_crate/http/
response.rs

1/*
2 * Licensed to Elasticsearch B.V. under one or more contributor
3 * license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright
5 * ownership. Elasticsearch B.V. licenses this file to you under
6 * the Apache License, Version 2.0 (the "License"); you may
7 * not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 *	http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied.  See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19
20/*
21 * SPDX-License-Identifier: Apache-2.0
22 *
23 * The OpenSearch Contributors require contributions made to
24 * this file be licensed under the Apache-2.0 license or a
25 * compatible open source license.
26 *
27 * Modifications Copyright OpenSearch Contributors. See
28 * GitHub history for details.
29 */
30
31//! HTTP response components
32use crate::{
33    error::Error as ClientError,
34    http::{headers::HeaderMap, Method, StatusCode, Url},
35};
36use bytes::Bytes;
37use serde::{
38    de,
39    de::{DeserializeOwned, MapAccess, Visitor},
40    Deserialize, Deserializer, Serialize,
41};
42use serde_json::Value;
43use std::{collections::BTreeMap, fmt, str::FromStr};
44use void::Void;
45
46/// A response from Elasticsearch
47pub struct Response {
48    response: reqwest::Response,
49    method: Method,
50}
51
52impl Response {
53    /// Creates a new instance of an Elasticsearch response
54    pub fn new(response: reqwest::Response, method: Method) -> Self {
55        Self {
56            response: response,
57            method: method,
58        }
59    }
60
61    /// Get the response content-length, if known.
62    ///
63    /// Reasons it may not be known:
64    ///
65    /// - The server didn't send a `content-length` header.
66    /// - The response is compressed and automatically decoded (thus changing
67    ///   the actual decoded length).
68    pub fn content_length(&self) -> Option<u64> {
69        self.response.content_length()
70    }
71
72    /// Gets the response content-type.
73    pub fn content_type(&self) -> &str {
74        self.response
75            .headers()
76            .get(crate::http::headers::CONTENT_TYPE)
77            .and_then(|value| value.to_str().ok())
78            .unwrap()
79    }
80
81    /// Turn the response into an [Error] if Elasticsearch returned an error.
82    pub fn error_for_status_code(self) -> Result<Self, ClientError> {
83        match self.response.error_for_status_ref() {
84            Ok(_) => Ok(self),
85            Err(err) => Err(err.into()),
86        }
87    }
88
89    /// Turn the response into an [Error] if Elasticsearch returned an error.
90    pub fn error_for_status_code_ref(&self) -> Result<&Self, ClientError> {
91        match self.response.error_for_status_ref() {
92            Ok(_) => Ok(self),
93            Err(err) => Err(err.into()),
94        }
95    }
96
97    /// Asynchronously reads the response body into an [Exception] if
98    /// Elasticsearch returned a HTTP status code in the 400-599 range.
99    ///
100    /// Reading the response body consumes `self`
101    pub async fn exception(self) -> Result<Option<Exception>, ClientError> {
102        if self.status_code().is_client_error() || self.status_code().is_server_error() {
103            let ex = self.json().await?;
104            Ok(Some(ex))
105        } else {
106            Ok(None)
107        }
108    }
109
110    /// Asynchronously reads the response body as JSON
111    ///
112    /// Reading the response body consumes `self`
113    pub async fn json<B>(self) -> Result<B, ClientError>
114    where
115        B: DeserializeOwned,
116    {
117        let body = self.response.json::<B>().await?;
118        Ok(body)
119    }
120
121    /// Gets the response headers.
122    pub fn headers(&self) -> &HeaderMap {
123        self.response.headers()
124    }
125
126    /// Gets the request method.
127    pub fn method(&self) -> Method {
128        self.method
129    }
130
131    /// Get the HTTP status code of the response
132    pub fn status_code(&self) -> StatusCode {
133        self.response.status()
134    }
135
136    /// Asynchronously reads the response body as plain text
137    ///
138    /// Reading the response body consumes `self`
139    pub async fn text(self) -> Result<String, ClientError> {
140        let body = self.response.text().await?;
141        Ok(body)
142    }
143
144    /// Asynchronously reads the response body as bytes
145    ///
146    /// Reading the response body consumes `self`
147    pub async fn bytes(self) -> Result<Bytes, ClientError> {
148        let bytes: Bytes = self.response.bytes().await?;
149        Ok(bytes)
150    }
151
152    /// Gets the request URL
153    pub fn url(&self) -> &Url {
154        self.response.url()
155    }
156
157    /// Gets the Deprecation warning response headers
158    ///
159    /// Deprecation headers signal the use of Elasticsearch functionality
160    /// or features that are deprecated and will be removed in a future release.
161    pub fn warning_headers(&self) -> impl Iterator<Item = &str> {
162        self.response.headers().get_all("Warning").iter().map(|w| {
163            let s = w.to_str().unwrap();
164            let first_quote = s.find('"').unwrap();
165            let last_quote = s.len() - 1;
166            &s[first_quote + 1..last_quote]
167        })
168    }
169}
170
171impl fmt::Debug for Response {
172    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
173        f.debug_struct("Response")
174            .field("method", &self.method())
175            .field("url", self.url())
176            .field("status_code", &self.status_code())
177            .field("headers", self.headers())
178            .finish()
179    }
180}
181
182/// An exception raised by Elasticsearch.
183///
184/// Contains details that indicate why the exception was raised which can help to determine
185/// what subsequent action to take.
186#[serde_with::skip_serializing_none]
187#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
188pub struct Exception {
189    status: Option<u16>,
190    #[serde(deserialize_with = "crate::string_or_struct")]
191    error: Error,
192}
193
194impl Exception {
195    /// The status code of the exception, if available.
196    pub fn status(&self) -> Option<u16> {
197        self.status
198    }
199
200    /// The details for the exception
201    pub fn error(&self) -> &Error {
202        &self.error
203    }
204}
205
206/// Details about the exception raised by Elasticsearch
207#[serde_with::skip_serializing_none]
208#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
209pub struct Error {
210    #[serde(deserialize_with = "option_box_cause", default)]
211    caused_by: Option<Box<Cause>>,
212    #[serde(default = "BTreeMap::new", deserialize_with = "header_map")]
213    header: BTreeMap<String, Vec<String>>,
214    #[serde(default = "Vec::new")]
215    root_cause: Vec<Cause>,
216    reason: Option<String>,
217    stack_trace: Option<String>,
218    #[serde(rename = "type")]
219    ty: Option<String>,
220    #[serde(default = "BTreeMap::new", flatten)]
221    additional_details: BTreeMap<String, Value>,
222}
223
224/// Deserializes the headers map where the map values may be a string or a sequence of strings
225fn header_map<'de, D>(deserializer: D) -> Result<BTreeMap<String, Vec<String>>, D::Error>
226where
227    D: Deserializer<'de>,
228{
229    #[derive(Deserialize)]
230    struct Wrapper(#[serde(deserialize_with = "crate::string_or_seq_string")] Vec<String>);
231
232    let v: BTreeMap<String, Wrapper> = BTreeMap::deserialize(deserializer)?;
233    Ok(v.into_iter().map(|(k, Wrapper(v))| (k, v)).collect())
234}
235
236impl Error {
237    /// The cause of the exception
238    pub fn caused_by(&self) -> Option<&Cause> {
239        self.caused_by.as_deref()
240    }
241
242    /// The root causes for the exception
243    pub fn root_cause(&self) -> &Vec<Cause> {
244        &self.root_cause
245    }
246
247    /// The headers for the exception
248    pub fn header(&self) -> &BTreeMap<String, Vec<String>> {
249        &self.header
250    }
251
252    /// The reason for the exception, if available.
253    pub fn reason(&self) -> Option<&str> {
254        self.reason.as_deref()
255    }
256
257    /// The exception stack trace, if available.
258    ///
259    /// Available if `error_trace` is specified on the request
260    pub fn stack_trace(&self) -> Option<&str> {
261        self.stack_trace.as_deref()
262    }
263
264    /// The type of exception, if available.
265    pub fn ty(&self) -> Option<&str> {
266        self.ty.as_deref()
267    }
268
269    /// Additional details about the cause.
270    ///
271    /// Elasticsearch can return additional details about an exception, depending
272    /// on context, which do not map to fields on [Error]. These are collected here
273    pub fn additional_details(&self) -> &BTreeMap<String, Value> {
274        &self.additional_details
275    }
276}
277
278// An error in an Elasticsearch exception can be returned as a simple message string only, or
279// as a JSON object. Handle both cases by corralling the simple message into the reason field
280impl FromStr for Error {
281    type Err = Void;
282
283    fn from_str(s: &str) -> Result<Self, Self::Err> {
284        Ok(Error {
285            caused_by: None,
286            header: Default::default(),
287            root_cause: Vec::new(),
288            reason: Some(s.to_string()),
289            stack_trace: None,
290            ty: None,
291            additional_details: Default::default(),
292        })
293    }
294}
295
296/// The cause of an exception
297#[serde_with::skip_serializing_none]
298#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
299pub struct Cause {
300    #[serde(deserialize_with = "option_box_cause", default)]
301    caused_by: Option<Box<Cause>>,
302    reason: Option<String>,
303    stack_trace: Option<String>,
304    #[serde(rename = "type")]
305    ty: Option<String>,
306    #[serde(default = "BTreeMap::new", flatten)]
307    additional_details: BTreeMap<String, Value>,
308}
309
310/// Deserializes a string or a map into Some boxed [Cause]. A missing field
311/// for `caused_by` is handled by serde's default attribute on the struct field,
312/// which will assign None to the field.
313fn option_box_cause<'de, D>(deserializer: D) -> Result<Option<Box<Cause>>, D::Error>
314where
315    D: Deserializer<'de>,
316{
317    struct CauseVisitor;
318    impl<'de> Visitor<'de> for CauseVisitor {
319        type Value = Cause;
320
321        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
322            formatter.write_str("string or map")
323        }
324
325        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
326        where
327            E: de::Error,
328        {
329            Ok(Cause {
330                caused_by: None,
331                reason: Some(value.to_string()),
332                stack_trace: None,
333                ty: None,
334                additional_details: Default::default(),
335            })
336        }
337
338        fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
339        where
340            M: MapAccess<'de>,
341        {
342            Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))
343        }
344    }
345
346    deserializer
347        .deserialize_any(CauseVisitor)
348        .map(|c| Some(Box::new(c)))
349}
350
351impl Cause {
352    /// The cause of the exception
353    pub fn caused_by(&self) -> Option<&Cause> {
354        self.caused_by.as_deref()
355    }
356
357    /// The reason for the exception
358    pub fn reason(&self) -> Option<&str> {
359        self.reason.as_deref()
360    }
361
362    /// The exception stack trace, if available.
363    ///
364    /// Available if `error_trace` is specified on the request
365    pub fn stack_trace(&self) -> Option<&str> {
366        self.stack_trace.as_deref()
367    }
368
369    /// The type of exception, if available.
370    pub fn ty(&self) -> Option<&str> {
371        self.ty.as_deref()
372    }
373
374    /// Additional details about the cause.
375    ///
376    /// Elasticsearch can return additional details about an exception, depending
377    /// on context, which do not map to fields on [Error]. These are collected here
378    pub fn additional_details(&self) -> &BTreeMap<String, Value> {
379        &self.additional_details
380    }
381}
382
383#[cfg(test)]
384pub mod tests {
385    use crate::http::response::Exception;
386    use serde_json::json;
387
388    #[test]
389    fn deserialize_error_string() -> Result<(), failure::Error> {
390        let json = r#"{"error":"no handler found for uri [/test_1/test/1/_update?_source=foo%2Cbar] and method [POST]"}"#;
391        let ex: Exception = serde_json::from_str(json)?;
392
393        assert_eq!(ex.status(), None);
394        assert_eq!(ex.error().reason(), Some("no handler found for uri [/test_1/test/1/_update?_source=foo%2Cbar] and method [POST]"));
395        assert_eq!(ex.error().ty(), None);
396
397        Ok(())
398    }
399
400    #[test]
401    fn deserialize_illegal_argument_exception() -> Result<(), failure::Error> {
402        let json = r#"{
403          "error": {
404            "root_cause": [{
405              "type": "illegal_argument_exception",
406              "reason": "Missing mandatory contexts in context query"
407            }],
408            "type": "search_phase_execution_exception",
409            "reason": "all shards failed",
410            "phase": "query",
411            "grouped": true,
412            "header": {
413                "WWW-Authenticate": "Bearer: token",
414                "x": ["y", "z"]
415            },
416            "failed_shards": [{
417              "shard": 0,
418              "index": "test",
419              "node": "APOkVK-rQi2Ll6CcAdeR6Q",
420              "reason": {
421                "type": "illegal_argument_exception",
422                "reason": "Missing mandatory contexts in context query"
423              }
424            }],
425            "caused_by": {
426              "type": "illegal_argument_exception",
427              "reason": "Missing mandatory contexts in context query",
428              "caused_by": {
429                "type": "illegal_argument_exception",
430                "reason": "Missing mandatory contexts in context query"
431              }
432            }
433          },
434          "status": 400
435        }"#;
436
437        let ex: Exception = serde_json::from_str(json)?;
438
439        assert_eq!(ex.status(), Some(400));
440
441        let error = ex.error();
442
443        assert_eq!(error.root_cause().len(), 1);
444        assert_eq!(
445            error.root_cause()[0].ty(),
446            Some("illegal_argument_exception")
447        );
448        assert_eq!(
449            error.root_cause()[0].reason(),
450            Some("Missing mandatory contexts in context query")
451        );
452
453        assert_eq!(error.header().len(), 2);
454        assert_eq!(
455            error.header().get("WWW-Authenticate"),
456            Some(&vec!["Bearer: token".to_string()])
457        );
458        assert_eq!(
459            error.header().get("x"),
460            Some(&vec!["y".to_string(), "z".to_string()])
461        );
462
463        assert!(error.caused_by().is_some());
464        let caused_by = error.caused_by().unwrap();
465
466        assert_eq!(caused_by.ty(), Some("illegal_argument_exception"));
467        assert_eq!(
468            caused_by.reason(),
469            Some("Missing mandatory contexts in context query")
470        );
471
472        assert!(caused_by.caused_by().is_some());
473        let caused_by_caused_by = caused_by.caused_by().unwrap();
474
475        assert_eq!(caused_by_caused_by.ty(), Some("illegal_argument_exception"));
476        assert_eq!(
477            caused_by_caused_by.reason(),
478            Some("Missing mandatory contexts in context query")
479        );
480
481        assert!(error.additional_details().len() > 0);
482        assert_eq!(
483            error.additional_details().get("phase"),
484            Some(&json!("query"))
485        );
486        assert_eq!(
487            error.additional_details().get("grouped"),
488            Some(&json!(true))
489        );
490
491        Ok(())
492    }
493
494    #[test]
495    fn deserialize_index_not_found_exception() -> Result<(), failure::Error> {
496        let json = r#"{
497          "error": {
498            "root_cause": [{
499              "type": "index_not_found_exception",
500              "reason": "no such index [test_index]",
501              "resource.type": "index_or_alias",
502              "resource.id": "test_index",
503              "index_uuid": "_na_",
504              "index": "test_index"
505            }],
506            "type": "index_not_found_exception",
507            "reason": "no such index [test_index]",
508            "resource.type": "index_or_alias",
509            "resource.id": "test_index",
510            "index_uuid": "_na_",
511            "index": "test_index"
512          },
513          "status": 404
514        }"#;
515
516        let ex: Exception = serde_json::from_str(json)?;
517
518        assert_eq!(ex.status(), Some(404));
519        let error = ex.error();
520
521        assert_eq!(error.ty(), Some("index_not_found_exception"));
522        assert_eq!(error.reason(), Some("no such index [test_index]"));
523        assert_eq!(
524            error.additional_details().get("index").unwrap(),
525            &json!("test_index")
526        );
527        assert_eq!(error.root_cause().len(), 1);
528        assert_eq!(
529            error.root_cause()[0].ty(),
530            Some("index_not_found_exception")
531        );
532        Ok(())
533    }
534}