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    /// Named path parameters captured by ServeMux wildcard patterns, e.g. `{id}`.
40    /// Populated by `ServeMux::serve_http`; empty for client-side requests.
41    pub path_params: HashMap<String, String>,
42    /// Parsed form values (populated by `parse_form`).
43    form: Option<HashMap<String, Vec<String>>>,
44    /// Cancellation context.
45    ctx: Context,
46}
47
48impl Request {
49    // ── Constructors ─────────────────────────────────────────────────────────
50
51    /// Create a new outgoing request.
52    /// Port of Go's `http.NewRequest`.
53    pub fn new(method: &str, url: &str, body: Option<Body>) -> Result<Self, HttpError> {
54        let ctx = go_lib::context::background();
55        Self::new_with_context(method, url, body, ctx)
56    }
57
58    /// Create a new outgoing request tied to a context.
59    /// Port of Go's `http.NewRequestWithContext`.
60    pub fn new_with_context(
61        method: &str,
62        url: &str,
63        body: Option<Body>,
64        ctx: Context,
65    ) -> Result<Self, HttpError> {
66        if !crate::method::is_valid(method) {
67            return Err(HttpError::InvalidUrl(format!("invalid method: {method}")));
68        }
69        let parsed = Url::parse(url).map_err(|e| HttpError::InvalidUrl(e.to_string()))?;
70        let host = parsed.host_str().unwrap_or("").to_owned();
71        let content_length = match &body {
72            None => 0,
73            Some(_) => -1,
74        };
75        Ok(Self {
76            method: method.to_owned(),
77            url: parsed,
78            proto: "HTTP/1.1".into(),
79            proto_major: 1,
80            proto_minor: 1,
81            header: Header::new(),
82            body,
83            content_length,
84            transfer_encoding: Vec::new(),
85            host,
86            trailer: Header::new(),
87            remote_addr: String::new(),
88            path_params: HashMap::new(),
89            form: None,
90            ctx,
91        })
92    }
93
94    // ── Context ───────────────────────────────────────────────────────────────
95
96    pub fn context(&self) -> &Context {
97        &self.ctx
98    }
99
100    /// Return a shallow clone of this request with the context replaced.
101    /// Port of Go's `(*Request).WithContext`.
102    pub fn with_context(mut self, ctx: Context) -> Self {
103        self.ctx = ctx;
104        self
105    }
106
107    // ── Path parameter helpers ────────────────────────────────────────────────
108
109    /// Return the value captured for the named wildcard in the matched ServeMux
110    /// pattern (e.g. `{id}` or `{path...}`).  Returns `""` if the parameter
111    /// was not captured.  Port of Go's `(*Request).PathValue`.
112    pub fn path_value(&self, name: &str) -> &str {
113        self.path_params.get(name).map(String::as_str).unwrap_or("")
114    }
115
116    // ── Header helpers ────────────────────────────────────────────────────────
117
118    pub fn user_agent(&self) -> &str {
119        self.header.get("User-Agent").unwrap_or("")
120    }
121
122    pub fn referer(&self) -> &str {
123        self.header.get("Referer").unwrap_or("")
124    }
125
126    /// Parse and return Basic Auth credentials.
127    /// Port of Go's `(*Request).BasicAuth`.
128    pub fn basic_auth(&self) -> Option<(String, String)> {
129        let val = self.header.get("Authorization")?;
130        let rest = val.strip_prefix("Basic ")?;
131        let decoded = base64::Engine::decode(
132            &base64::engine::general_purpose::STANDARD,
133            rest.trim(),
134        )
135        .ok()?;
136        let s = String::from_utf8(decoded).ok()?;
137        let colon = s.find(':')?;
138        Some((s[..colon].to_owned(), s[colon + 1..].to_owned()))
139    }
140
141    // ── Cookie helpers ────────────────────────────────────────────────────────
142
143    /// Return all cookies sent with the request.
144    pub fn cookies(&self) -> Vec<crate::cookie::Cookie> {
145        crate::cookie::parse_request_cookies(&self.header)
146    }
147
148    /// Return the named cookie, or `None`.
149    pub fn cookie(&self, name: &str) -> Option<crate::cookie::Cookie> {
150        self.cookies().into_iter().find(|c| c.name == name)
151    }
152
153    // ── Body helpers ─────────────────────────────────────────────────────────
154
155    /// Read the entire request body, populate `self.trailer` from any chunked
156    /// trailer headers, and return the raw bytes.
157    pub fn body_bytes(&mut self) -> Result<Vec<u8>, HttpError> {
158        let mut out = Vec::new();
159        if let Some(ref mut body) = self.body {
160            body.read_to_end(&mut out).map_err(|_| HttpError::BodyRead)?;
161            self.trailer = body.trailers().clone();
162        }
163        Ok(out)
164    }
165
166    /// Trailer headers declared by the client.  Populated after the body has
167    /// been fully read via `body_bytes` or a manual `read_to_end`.
168    pub fn trailers(&self) -> &Header {
169        &self.trailer
170    }
171
172    // ── Form parsing ──────────────────────────────────────────────────────────
173
174    /// Parse application/x-www-form-urlencoded body or query string.
175    /// Port of Go's `(*Request).ParseForm`.
176    pub fn parse_form(&mut self) -> Result<(), HttpError> {
177        if self.form.is_some() {
178            return Ok(());
179        }
180        let mut values: HashMap<String, Vec<String>> = HashMap::new();
181
182        // Query string.
183        for (k, v) in self.url.query_pairs() {
184            values.entry(k.into_owned()).or_default().push(v.into_owned());
185        }
186
187        // Body (only for POST/PUT/PATCH with the right content-type).
188        let ct = self
189            .header
190            .get("Content-Type")
191            .unwrap_or("")
192            .to_ascii_lowercase();
193        if matches!(self.method.as_str(), "POST" | "PUT" | "PATCH")
194            && ct.starts_with("application/x-www-form-urlencoded")
195            && let Some(body) = self.body.take()
196        {
197            let mut raw = Vec::new();
198            BodyReader(body)
199                .read_to_end(&mut raw)
200                .map_err(|_| HttpError::BodyRead)?;
201            let s = String::from_utf8_lossy(&raw);
202            for pair in s.split('&') {
203                if let Some((k, v)) = pair.split_once('=') {
204                    let k = url_decode(k);
205                    let v = url_decode(v);
206                    values.entry(k).or_default().push(v);
207                }
208            }
209        }
210
211        self.form = Some(values);
212        Ok(())
213    }
214
215    /// Return a form value by key (after calling `parse_form`).
216    pub fn form_value(&self, key: &str) -> Option<&str> {
217        self.form
218            .as_ref()?
219            .get(key)?
220            .first()
221            .map(String::as_str)
222    }
223
224    // ── Wire serialization ────────────────────────────────────────────────────
225
226    /// Serialize the request line and headers to `w` (body not included).
227    /// Port of Go's `(*Request).write`.
228    pub fn write_header_to(&self, w: &mut impl std::io::Write) -> Result<(), HttpError> {
229        let path = if self.url.path().is_empty() { "/" } else { self.url.path() };
230        let query = self
231            .url
232            .query()
233            .map(|q| format!("?{q}"))
234            .unwrap_or_default();
235
236        write!(w, "{} {}{} {}\r\n", self.method, path, query, self.proto)?;
237        write!(w, "Host: {}\r\n", self.host)?;
238        self.header.write_to(w)?;
239        w.write_all(b"\r\n")?;
240        Ok(())
241    }
242
243    /// Like [`write_header_to`][Self::write_header_to] but emits the
244    /// request-target in **absolute-form** (`GET http://host/path HTTP/1.1`),
245    /// as required when sending a request to an HTTP proxy (RFC 7230 §5.3.2).
246    pub fn write_header_absolute_to(&self, w: &mut impl std::io::Write) -> Result<(), HttpError> {
247        write!(w, "{} {} {}\r\n", self.method, self.absolute_target(), self.proto)?;
248        write!(w, "Host: {}\r\n", self.host)?;
249        self.header.write_to(w)?;
250        w.write_all(b"\r\n")?;
251        Ok(())
252    }
253
254    /// Reconstruct the absolute request-target `scheme://host[:port]/path?query`
255    /// (without userinfo or fragment) for proxied requests.
256    fn absolute_target(&self) -> String {
257        let u = &self.url;
258        let authority = match (u.host_str(), u.port()) {
259            (Some(h), Some(p)) => format!("{h}:{p}"),
260            (Some(h), None)    => h.to_owned(),
261            (None, _)          => String::new(),
262        };
263        let path  = if u.path().is_empty() { "/" } else { u.path() };
264        let query = u.query().map(|q| format!("?{q}")).unwrap_or_default();
265        format!("{}://{}{}{}", u.scheme(), authority, path, query)
266    }
267}
268
269// Helper: allow Body to be read via std::io::Read without exposing internals.
270struct BodyReader(Body);
271impl Read for BodyReader {
272    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
273        self.0.read(buf)
274    }
275}
276
277fn url_decode(s: &str) -> String {
278    // Simple + → space, then percent-decode.
279    let s = s.replace('+', " ");
280    url::form_urlencoded::parse(s.as_bytes())
281        .map(|(k, _)| k.into_owned())
282        .next()
283        .unwrap_or(s.clone())
284}
285
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn new_get_request() {
293        let req = Request::new("GET", "http://example.com/path?q=1", None).unwrap();
294        assert_eq!(req.method, "GET");
295        assert_eq!(req.host, "example.com");
296        assert_eq!(req.url.path(), "/path");
297    }
298
299    #[test]
300    fn invalid_method_rejected() {
301        assert!(Request::new("GÉT", "http://example.com/", None).is_err());
302    }
303
304    #[test]
305    fn write_header() {
306        let mut req = Request::new("GET", "http://example.com/", None).unwrap();
307        req.header.set("Accept", "text/html");
308        let mut out = Vec::new();
309        req.write_header_to(&mut out).unwrap();
310        let s = String::from_utf8(out).unwrap();
311        assert!(s.starts_with("GET / HTTP/1.1\r\n"));
312        assert!(s.contains("Host: example.com\r\n"));
313        assert!(s.contains("Accept: text/html\r\n"));
314    }
315
316    #[test]
317    fn write_header_includes_query() {
318        let req = Request::new("GET", "http://example.com/search?q=rust&n=1", None).unwrap();
319        let mut out = Vec::new();
320        req.write_header_to(&mut out).unwrap();
321        let s = String::from_utf8(out).unwrap();
322        assert!(s.starts_with("GET /search?q=rust&n=1 HTTP/1.1\r\n"), "got: {s:?}");
323    }
324
325    #[test]
326    fn body_sets_content_length_unknown() {
327        let body = Body::Unbounded(Box::new(std::io::Cursor::new(b"abc".to_vec())));
328        let req = Request::new("POST", "http://example.com/", Some(body)).unwrap();
329        assert_eq!(req.content_length, -1);
330        let none = Request::new("GET", "http://example.com/", None).unwrap();
331        assert_eq!(none.content_length, 0);
332    }
333
334    #[test]
335    fn parse_form_query_only() {
336        let mut req = Request::new("GET", "http://x/p?a=1&a=2&b=hello", None).unwrap();
337        req.parse_form().unwrap();
338        assert_eq!(req.form_value("a"), Some("1"));
339        assert_eq!(req.form_value("b"), Some("hello"));
340        assert_eq!(req.form_value("missing"), None);
341        // Idempotent: a second call is a no-op.
342        req.parse_form().unwrap();
343        assert_eq!(req.form_value("a"), Some("1"));
344    }
345
346    #[test]
347    fn parse_form_urlencoded_body() {
348        let body = Body::Unbounded(Box::new(std::io::Cursor::new(
349            b"name=Alice&city=New+York".to_vec(),
350        )));
351        let mut req = Request::new("POST", "http://x/submit?q=top", Some(body)).unwrap();
352        req.header.set("Content-Type", "application/x-www-form-urlencoded");
353        req.parse_form().unwrap();
354        // Query and body values are merged.
355        assert_eq!(req.form_value("q"), Some("top"));
356        assert_eq!(req.form_value("name"), Some("Alice"));
357        assert_eq!(req.form_value("city"), Some("New York"));
358    }
359
360    #[test]
361    fn basic_auth_valid() {
362        use base64::Engine;
363        let creds = base64::engine::general_purpose::STANDARD.encode("alice:s3cret");
364        let mut req = Request::new("GET", "http://x/", None).unwrap();
365        req.header.set("Authorization", format!("Basic {creds}"));
366        let (user, pass) = req.basic_auth().unwrap();
367        assert_eq!(user, "alice");
368        assert_eq!(pass, "s3cret");
369    }
370
371    #[test]
372    fn basic_auth_absent_or_wrong_scheme() {
373        let req = Request::new("GET", "http://x/", None).unwrap();
374        assert!(req.basic_auth().is_none());
375
376        let mut bearer = Request::new("GET", "http://x/", None).unwrap();
377        bearer.header.set("Authorization", "Bearer token123");
378        assert!(bearer.basic_auth().is_none());
379    }
380
381    #[test]
382    fn user_agent_and_referer() {
383        let mut req = Request::new("GET", "http://x/", None).unwrap();
384        assert_eq!(req.user_agent(), "");
385        assert_eq!(req.referer(), "");
386        req.header.set("User-Agent", "go-http/0.1");
387        req.header.set("Referer", "http://ref/");
388        assert_eq!(req.user_agent(), "go-http/0.1");
389        assert_eq!(req.referer(), "http://ref/");
390    }
391
392    #[test]
393    fn cookies_parsed_from_header() {
394        let mut req = Request::new("GET", "http://x/", None).unwrap();
395        req.header.set("Cookie", "session=abc; theme=dark");
396        let all = req.cookies();
397        assert_eq!(all.len(), 2);
398        assert_eq!(req.cookie("session").unwrap().value, "abc");
399        assert_eq!(req.cookie("theme").unwrap().value, "dark");
400        assert!(req.cookie("nope").is_none());
401    }
402
403    #[test]
404    fn with_context_replaces_context() {
405        let req = Request::new("GET", "http://x/", None).unwrap();
406        let ctx = go_lib::context::background();
407        let req = req.with_context(ctx);
408        // The context accessor returns the replacement without panicking.
409        let _ = req.context();
410    }
411}