Skip to main content

iceberg_catalog_rest/
response.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! The response type REST catalog requests come back as.
19
20use std::fmt::{Debug, Formatter};
21
22use http::{HeaderMap, StatusCode};
23use iceberg::Result;
24use reqwest::Response;
25
26use crate::client::format_headers_redacted;
27
28/// A REST catalog response, read into memory.
29///
30/// The counterpart of [`HttpRequest`](crate::HttpRequest): it keeps the
31/// concrete client type inside [`HttpClient`](crate::HttpClient) so callers
32/// work with the stable `http` crate types instead. Catalog responses are
33/// small JSON documents that every caller reads in full, so the body is
34/// buffered rather than streamed.
35#[derive(Clone)]
36pub struct HttpResponse {
37    status: StatusCode,
38    headers: HeaderMap,
39    body: Vec<u8>,
40}
41
42impl Debug for HttpResponse {
43    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
44        // A token exchange answers with a credential in the body, and headers
45        // may carry `set-cookie`, so both are held back.
46        f.debug_struct("HttpResponse")
47            .field("status", &self.status)
48            // Always redacted: a response carries no config, and the
49            // `disable-header-redaction` escape hatch is for the request side.
50            .field("headers", &format_headers_redacted(&self.headers, false))
51            .field("body", &format_args!("{} bytes", self.body.len()))
52            .finish()
53    }
54}
55
56impl HttpResponse {
57    /// Reads `response` into memory.
58    pub(crate) async fn read(response: Response) -> Result<Self> {
59        let status = response.status();
60        let headers = response.headers().clone();
61        // `bytes()` builds its error without a URL, so keep the one the
62        // response came from.
63        let url = response.url().clone();
64        Ok(Self {
65            status,
66            headers,
67            body: response
68                .bytes()
69                .await
70                .map_err(|err| err.with_url(url))?
71                .to_vec(),
72        })
73    }
74
75    /// Builds a response, e.g. to unit-test code that consumes one.
76    pub fn new(status: StatusCode, headers: HeaderMap, body: Vec<u8>) -> Self {
77        Self {
78            status,
79            headers,
80            body,
81        }
82    }
83
84    /// The response status.
85    pub fn status(&self) -> StatusCode {
86        self.status
87    }
88
89    /// The response headers.
90    pub fn headers(&self) -> &HeaderMap {
91        &self.headers
92    }
93
94    /// The response body.
95    pub fn body(&self) -> &[u8] {
96        &self.body
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_debug_holds_back_secrets() {
106        // The body of a token exchange is a credential, and `set-cookie` is
107        // redacted everywhere else in this crate.
108        let mut headers = HeaderMap::new();
109        headers.insert("set-cookie", "session=secret".parse().unwrap());
110        headers.insert("content-type", "application/json".parse().unwrap());
111        let debug = format!(
112            "{:?}",
113            HttpResponse::new(
114                StatusCode::OK,
115                headers,
116                br#"{"access_token": "tok"}"#.to_vec()
117            )
118        );
119
120        assert!(!debug.contains("secret"), "{debug}");
121        assert!(!debug.contains("tok"), "{debug}");
122        assert!(debug.contains("content-type"), "{debug}");
123        assert!(debug.contains("200"), "{debug}");
124    }
125
126    #[test]
127    fn test_new_exposes_what_it_was_built_from() {
128        let mut headers = HeaderMap::new();
129        headers.insert("content-type", "application/json".parse().unwrap());
130        let response = HttpResponse::new(StatusCode::OK, headers, "{}".as_bytes().to_vec());
131
132        assert_eq!(response.status(), StatusCode::OK);
133        assert_eq!(
134            response.headers().get("content-type").unwrap(),
135            "application/json"
136        );
137        assert_eq!(response.body(), b"{}");
138    }
139}