hurl/http/
request.rs

1/*
2 * Hurl (https://hurl.dev)
3 * Copyright (C) 2025 Orange
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * 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, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 */
18use std::fmt;
19
20use crate::http::header::{HeaderVec, COOKIE};
21use crate::http::url::Url;
22use crate::http::RequestCookie;
23
24/// Represents a runtime HTTP request.
25/// This is a real request, that has been executed by our HTTP client.
26/// It's different from `crate::http::RequestSpec` which is the request asked to be executed by our
27/// user. For instance, in the request spec, headers implicitly added by curl are not present, while
28/// they will be present in the [`Request`] instances.
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct Request {
31    /// Absolute URL.
32    pub url: Url,
33    /// Method.
34    pub method: String,
35    /// List of HTTP headers.
36    pub headers: HeaderVec,
37    /// Response body bytes.
38    pub body: Vec<u8>,
39}
40
41#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
42pub enum RequestedHttpVersion {
43    /// The effective HTTP version will be chosen by libcurl
44    #[default]
45    Default,
46    Http10,
47    Http11,
48    Http2,
49    Http3,
50}
51
52impl fmt::Display for RequestedHttpVersion {
53    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
54        let value = match self {
55            RequestedHttpVersion::Default => "HTTP (default)",
56            RequestedHttpVersion::Http10 => "HTTP/1.0",
57            RequestedHttpVersion::Http11 => "HTTP/1.1",
58            RequestedHttpVersion::Http2 => "HTTP/2",
59            RequestedHttpVersion::Http3 => "HTTP/3",
60        };
61        write!(f, "{value}")
62    }
63}
64
65#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
66pub enum IpResolve {
67    /// Default, can use addresses of all IP versions that your system allows.
68    #[default]
69    Default,
70    IpV4,
71    IpV6,
72}
73
74impl Request {
75    /// Creates a new request.
76    pub fn new(method: &str, url: Url, headers: HeaderVec, body: Vec<u8>) -> Self {
77        Request {
78            url,
79            method: method.to_string(),
80            headers,
81            body,
82        }
83    }
84
85    /// Returns a list of request headers cookie.
86    ///
87    /// see <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie>
88    pub fn cookies(&self) -> Vec<RequestCookie> {
89        self.headers
90            .get_all(COOKIE)
91            .iter()
92            .flat_map(|h| parse_cookies(h.value.as_str().trim()))
93            .collect()
94    }
95}
96
97fn parse_cookies(s: &str) -> Vec<RequestCookie> {
98    s.split(';').map(|t| parse_cookie(t.trim())).collect()
99}
100
101fn parse_cookie(s: &str) -> RequestCookie {
102    match s.find('=') {
103        Some(i) => RequestCookie {
104            name: s.split_at(i).0.to_string(),
105            value: s.split_at(i + 1).1.to_string(),
106        },
107        None => RequestCookie {
108            name: s.to_string(),
109            value: String::new(),
110        },
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use crate::http::{Header, RequestCookie};
118
119    fn hello_request() -> Request {
120        let mut headers = HeaderVec::new();
121        headers.push(Header::new("Host", "localhost:8000"));
122        headers.push(Header::new("Accept", "*/*"));
123        headers.push(Header::new("User-Agent", "hurl/1.0"));
124        headers.push(Header::new("content-type", "application/json"));
125        let url = "http://localhost:8000/hello".parse().unwrap();
126
127        Request::new("GET", url, headers, vec![])
128    }
129
130    fn query_string_request() -> Request {
131        let url = "http://localhost:8000/querystring-params?param1=value1&param2=&param3=a%3Db&param4=1%2C2%2C3".parse().unwrap();
132
133        Request::new("GET", url, HeaderVec::new(), vec![])
134    }
135
136    fn cookies_request() -> Request {
137        let mut headers = HeaderVec::new();
138        headers.push(Header::new("Cookie", "cookie1=value1; cookie2=value2"));
139        let url = "http://localhost:8000/cookies".parse().unwrap();
140        Request::new("GET", url, headers, vec![])
141    }
142
143    #[test]
144    fn test_content_type() {
145        assert_eq!(
146            hello_request().headers.content_type(),
147            Some("application/json")
148        );
149        assert_eq!(query_string_request().headers.content_type(), None);
150        assert_eq!(cookies_request().headers.content_type(), None);
151    }
152
153    #[test]
154    fn test_cookies() {
155        assert!(hello_request().cookies().is_empty());
156        assert_eq!(
157            cookies_request().cookies(),
158            vec![
159                RequestCookie {
160                    name: "cookie1".to_string(),
161                    value: "value1".to_string(),
162                },
163                RequestCookie {
164                    name: "cookie2".to_string(),
165                    value: "value2".to_string(),
166                },
167            ]
168        );
169    }
170
171    #[test]
172    fn test_parse_cookies() {
173        assert_eq!(
174            parse_cookies("cookie1=value1; cookie2=value2"),
175            vec![
176                RequestCookie {
177                    name: "cookie1".to_string(),
178                    value: "value1".to_string(),
179                },
180                RequestCookie {
181                    name: "cookie2".to_string(),
182                    value: "value2".to_string(),
183                },
184            ]
185        );
186    }
187
188    #[test]
189    fn test_parse_cookie() {
190        assert_eq!(
191            parse_cookie("cookie1=value1"),
192            RequestCookie {
193                name: "cookie1".to_string(),
194                value: "value1".to_string(),
195            },
196        );
197    }
198}