Skip to main content

go_http/
request.rs

1// SPDX-License-Identifier: Apache-2.0
2
3/// Request — port of Go's `net/http.Request`.
4use std::collections::HashMap;
5use std::io::Read;
6
7use url::Url;
8
9use go_lib::context::Context;
10use crate::error::HttpError;
11use crate::header::Header;
12use crate::parse::transfer::Body;
13
14/// An HTTP request (incoming server-side or outgoing client-side).
15/// Mirrors Go's `http.Request`.
16pub struct Request {
17    /// HTTP method (GET, POST, …).
18    pub method: String,
19    /// Parsed request URL.
20    pub url: Url,
21    /// Protocol version string, e.g. "HTTP/1.1".
22    pub proto: String,
23    pub proto_major: u8,
24    pub proto_minor: u8,
25    /// Request headers.
26    pub header: Header,
27    /// Request body; `None` after the body has been consumed or for bodyless methods.
28    pub body: Option<Body>,
29    /// -1 means unknown; ≥ 0 means exact byte count from Content-Length.
30    pub content_length: i64,
31    /// Transfer-Encoding values in order (e.g. ["chunked"]).
32    pub transfer_encoding: Vec<String>,
33    /// Value of the Host header (or from the URL for outgoing requests).
34    pub host: String,
35    /// Trailing headers populated after a chunked body is fully read.
36    pub trailer: Header,
37    /// Remote address of the client (set by the server, empty on client requests).
38    pub remote_addr: String,
39    /// Parsed form values (populated by `parse_form`).
40    form: Option<HashMap<String, Vec<String>>>,
41    /// Cancellation context.
42    ctx: Context,
43}
44
45impl Request {
46    // ── Constructors ─────────────────────────────────────────────────────────
47
48    /// Create a new outgoing request.
49    /// Port of Go's `http.NewRequest`.
50    pub fn new(method: &str, url: &str, body: Option<Body>) -> Result<Self, HttpError> {
51        let ctx = go_lib::context::background();
52        Self::new_with_context(method, url, body, ctx)
53    }
54
55    /// Create a new outgoing request tied to a context.
56    /// Port of Go's `http.NewRequestWithContext`.
57    pub fn new_with_context(
58        method: &str,
59        url: &str,
60        body: Option<Body>,
61        ctx: Context,
62    ) -> Result<Self, HttpError> {
63        if !crate::method::is_valid(method) {
64            return Err(HttpError::InvalidUrl(format!("invalid method: {method}")));
65        }
66        let parsed = Url::parse(url).map_err(|e| HttpError::InvalidUrl(e.to_string()))?;
67        let host = parsed.host_str().unwrap_or("").to_owned();
68        let content_length = match &body {
69            None => 0,
70            Some(_) => -1,
71        };
72        Ok(Self {
73            method: method.to_owned(),
74            url: parsed,
75            proto: "HTTP/1.1".into(),
76            proto_major: 1,
77            proto_minor: 1,
78            header: Header::new(),
79            body,
80            content_length,
81            transfer_encoding: Vec::new(),
82            host,
83            trailer: Header::new(),
84            remote_addr: String::new(),
85            form: None,
86            ctx,
87        })
88    }
89
90    // ── Context ───────────────────────────────────────────────────────────────
91
92    pub fn context(&self) -> &Context {
93        &self.ctx
94    }
95
96    /// Return a shallow clone of this request with the context replaced.
97    /// Port of Go's `(*Request).WithContext`.
98    pub fn with_context(mut self, ctx: Context) -> Self {
99        self.ctx = ctx;
100        self
101    }
102
103    // ── Header helpers ────────────────────────────────────────────────────────
104
105    pub fn user_agent(&self) -> &str {
106        self.header.get("User-Agent").unwrap_or("")
107    }
108
109    pub fn referer(&self) -> &str {
110        self.header.get("Referer").unwrap_or("")
111    }
112
113    /// Parse and return Basic Auth credentials.
114    /// Port of Go's `(*Request).BasicAuth`.
115    pub fn basic_auth(&self) -> Option<(String, String)> {
116        let val = self.header.get("Authorization")?;
117        let rest = val.strip_prefix("Basic ")?;
118        let decoded = base64::Engine::decode(
119            &base64::engine::general_purpose::STANDARD,
120            rest.trim(),
121        )
122        .ok()?;
123        let s = String::from_utf8(decoded).ok()?;
124        let colon = s.find(':')?;
125        Some((s[..colon].to_owned(), s[colon + 1..].to_owned()))
126    }
127
128    // ── Cookie helpers ────────────────────────────────────────────────────────
129
130    /// Return all cookies sent with the request.
131    pub fn cookies(&self) -> Vec<crate::cookie::Cookie> {
132        crate::cookie::parse_request_cookies(&self.header)
133    }
134
135    /// Return the named cookie, or `None`.
136    pub fn cookie(&self, name: &str) -> Option<crate::cookie::Cookie> {
137        self.cookies().into_iter().find(|c| c.name == name)
138    }
139
140    // ── Form parsing ──────────────────────────────────────────────────────────
141
142    /// Parse application/x-www-form-urlencoded body or query string.
143    /// Port of Go's `(*Request).ParseForm`.
144    pub fn parse_form(&mut self) -> Result<(), HttpError> {
145        if self.form.is_some() {
146            return Ok(());
147        }
148        let mut values: HashMap<String, Vec<String>> = HashMap::new();
149
150        // Query string.
151        for (k, v) in self.url.query_pairs() {
152            values.entry(k.into_owned()).or_default().push(v.into_owned());
153        }
154
155        // Body (only for POST/PUT/PATCH with the right content-type).
156        let ct = self
157            .header
158            .get("Content-Type")
159            .unwrap_or("")
160            .to_ascii_lowercase();
161        if matches!(self.method.as_str(), "POST" | "PUT" | "PATCH")
162            && ct.starts_with("application/x-www-form-urlencoded")
163        {
164            if let Some(body) = self.body.take() {
165                let mut raw = Vec::new();
166                BodyReader(body)
167                    .read_to_end(&mut raw)
168                    .map_err(|_| HttpError::BodyRead)?;
169                let s = String::from_utf8_lossy(&raw);
170                for pair in s.split('&') {
171                    if let Some((k, v)) = pair.split_once('=') {
172                        let k = url_decode(k);
173                        let v = url_decode(v);
174                        values.entry(k).or_default().push(v);
175                    }
176                }
177            }
178        }
179
180        self.form = Some(values);
181        Ok(())
182    }
183
184    /// Return a form value by key (after calling `parse_form`).
185    pub fn form_value(&self, key: &str) -> Option<&str> {
186        self.form
187            .as_ref()?
188            .get(key)?
189            .first()
190            .map(String::as_str)
191    }
192
193    // ── Wire serialization ────────────────────────────────────────────────────
194
195    /// Serialize the request line and headers to `w` (body not included).
196    /// Port of Go's `(*Request).write`.
197    pub fn write_header_to(&self, w: &mut impl std::io::Write) -> Result<(), HttpError> {
198        let path = if self.url.path().is_empty() { "/" } else { self.url.path() };
199        let query = self
200            .url
201            .query()
202            .map(|q| format!("?{q}"))
203            .unwrap_or_default();
204
205        write!(w, "{} {}{} {}\r\n", self.method, path, query, self.proto)?;
206        write!(w, "Host: {}\r\n", self.host)?;
207        self.header.write_to(w)?;
208        w.write_all(b"\r\n")?;
209        Ok(())
210    }
211}
212
213// Helper: allow Body to be read via std::io::Read without exposing internals.
214struct BodyReader(Body);
215impl Read for BodyReader {
216    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
217        self.0.read(buf)
218    }
219}
220
221fn url_decode(s: &str) -> String {
222    // Simple + → space, then percent-decode.
223    let s = s.replace('+', " ");
224    url::form_urlencoded::parse(s.as_bytes())
225        .map(|(k, _)| k.into_owned())
226        .next()
227        .unwrap_or(s.clone())
228}
229
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn new_get_request() {
237        let req = Request::new("GET", "http://example.com/path?q=1", None).unwrap();
238        assert_eq!(req.method, "GET");
239        assert_eq!(req.host, "example.com");
240        assert_eq!(req.url.path(), "/path");
241    }
242
243    #[test]
244    fn invalid_method_rejected() {
245        assert!(Request::new("GÉT", "http://example.com/", None).is_err());
246    }
247
248    #[test]
249    fn write_header() {
250        let mut req = Request::new("GET", "http://example.com/", None).unwrap();
251        req.header.set("Accept", "text/html");
252        let mut out = Vec::new();
253        req.write_header_to(&mut out).unwrap();
254        let s = String::from_utf8(out).unwrap();
255        assert!(s.starts_with("GET / HTTP/1.1\r\n"));
256        assert!(s.contains("Host: example.com\r\n"));
257        assert!(s.contains("Accept: text/html\r\n"));
258    }
259
260    #[test]
261    fn write_header_includes_query() {
262        let req = Request::new("GET", "http://example.com/search?q=rust&n=1", None).unwrap();
263        let mut out = Vec::new();
264        req.write_header_to(&mut out).unwrap();
265        let s = String::from_utf8(out).unwrap();
266        assert!(s.starts_with("GET /search?q=rust&n=1 HTTP/1.1\r\n"), "got: {s:?}");
267    }
268
269    #[test]
270    fn body_sets_content_length_unknown() {
271        let body = Body::Unbounded(Box::new(std::io::Cursor::new(b"abc".to_vec())));
272        let req = Request::new("POST", "http://example.com/", Some(body)).unwrap();
273        assert_eq!(req.content_length, -1);
274        let none = Request::new("GET", "http://example.com/", None).unwrap();
275        assert_eq!(none.content_length, 0);
276    }
277
278    #[test]
279    fn parse_form_query_only() {
280        let mut req = Request::new("GET", "http://x/p?a=1&a=2&b=hello", None).unwrap();
281        req.parse_form().unwrap();
282        assert_eq!(req.form_value("a"), Some("1"));
283        assert_eq!(req.form_value("b"), Some("hello"));
284        assert_eq!(req.form_value("missing"), None);
285        // Idempotent: a second call is a no-op.
286        req.parse_form().unwrap();
287        assert_eq!(req.form_value("a"), Some("1"));
288    }
289
290    #[test]
291    fn parse_form_urlencoded_body() {
292        let body = Body::Unbounded(Box::new(std::io::Cursor::new(
293            b"name=Alice&city=New+York".to_vec(),
294        )));
295        let mut req = Request::new("POST", "http://x/submit?q=top", Some(body)).unwrap();
296        req.header.set("Content-Type", "application/x-www-form-urlencoded");
297        req.parse_form().unwrap();
298        // Query and body values are merged.
299        assert_eq!(req.form_value("q"), Some("top"));
300        assert_eq!(req.form_value("name"), Some("Alice"));
301        assert_eq!(req.form_value("city"), Some("New York"));
302    }
303
304    #[test]
305    fn basic_auth_valid() {
306        use base64::Engine;
307        let creds = base64::engine::general_purpose::STANDARD.encode("alice:s3cret");
308        let mut req = Request::new("GET", "http://x/", None).unwrap();
309        req.header.set("Authorization", format!("Basic {creds}"));
310        let (user, pass) = req.basic_auth().unwrap();
311        assert_eq!(user, "alice");
312        assert_eq!(pass, "s3cret");
313    }
314
315    #[test]
316    fn basic_auth_absent_or_wrong_scheme() {
317        let req = Request::new("GET", "http://x/", None).unwrap();
318        assert!(req.basic_auth().is_none());
319
320        let mut bearer = Request::new("GET", "http://x/", None).unwrap();
321        bearer.header.set("Authorization", "Bearer token123");
322        assert!(bearer.basic_auth().is_none());
323    }
324
325    #[test]
326    fn user_agent_and_referer() {
327        let mut req = Request::new("GET", "http://x/", None).unwrap();
328        assert_eq!(req.user_agent(), "");
329        assert_eq!(req.referer(), "");
330        req.header.set("User-Agent", "go-http/0.1");
331        req.header.set("Referer", "http://ref/");
332        assert_eq!(req.user_agent(), "go-http/0.1");
333        assert_eq!(req.referer(), "http://ref/");
334    }
335
336    #[test]
337    fn cookies_parsed_from_header() {
338        let mut req = Request::new("GET", "http://x/", None).unwrap();
339        req.header.set("Cookie", "session=abc; theme=dark");
340        let all = req.cookies();
341        assert_eq!(all.len(), 2);
342        assert_eq!(req.cookie("session").unwrap().value, "abc");
343        assert_eq!(req.cookie("theme").unwrap().value, "dark");
344        assert!(req.cookie("nope").is_none());
345    }
346
347    #[test]
348    fn with_context_replaces_context() {
349        let req = Request::new("GET", "http://x/", None).unwrap();
350        let ctx = go_lib::context::background();
351        let req = req.with_context(ctx);
352        // The context accessor returns the replacement without panicking.
353        let _ = req.context();
354    }
355}