Skip to main content

multistore_cf_workers/
request.rs

1//! Request parsing helpers for Cloudflare Workers.
2//!
3//! Provides [`RequestParts`] to extract owned HTTP metadata from a
4//! `web_sys::Request`, and convert it into the borrowed
5//! [`RequestInfo`](multistore::route_handler::RequestInfo) required by the gateway.
6
7use crate::body::JsBody;
8use crate::response::headermap_from_js;
9use http::{HeaderMap, Method, Uri};
10use multistore::route_handler::RequestInfo;
11
12/// Owned HTTP request metadata extracted from a `web_sys::Request`.
13///
14/// Workers passes a `web_sys::Request` with borrowed JS strings and a
15/// `ReadableStream` body.  The gateway expects a [`RequestInfo`] that
16/// borrows from Rust-owned data, so this struct bridges the gap by
17/// owning the parsed method, path, query, and headers.
18///
19/// # Example
20///
21/// ```rust,ignore
22/// let (parts, body) = RequestParts::from_web_sys(&req)?;
23/// let result = gateway
24///     .handle_request(&parts.as_request_info(), body, collect_js_body)
25///     .await;
26/// ```
27pub struct RequestParts {
28    /// The HTTP method.
29    pub method: Method,
30    /// The percent-**decoded** URL path (e.g. `"/bucket/my key"`).
31    ///
32    /// Decoded for S3 operation parsing and bucket/key routing. **Do not** use
33    /// this for SigV4 verification: the canonical URI must be the encoded form
34    /// the client signed, so use [`signing_path`](Self::signing_path) instead.
35    pub path: String,
36    /// The raw, percent-**encoded** URL path exactly as it arrived on the wire
37    /// (e.g. `"/bucket/my%20key"`).
38    ///
39    /// This is the form the client signs, so it is what SigV4 verification must
40    /// canonicalize over. [`as_request_info`](Self::as_request_info) wires it
41    /// into [`RequestInfo`]'s signing path automatically. Integrators that
42    /// rewrite paths before dispatch (e.g. path-mapping) must still sign against
43    /// this encoded path — never the decoded [`path`](Self::path).
44    pub signing_path: String,
45    /// The raw query string, if present.
46    pub query: Option<String>,
47    /// The HTTP request headers.
48    pub headers: HeaderMap,
49}
50
51impl RequestParts {
52    /// Parse a `web_sys::Request` into owned request metadata and a
53    /// zero-copy [`JsBody`].
54    ///
55    /// Extracts the body stream **before** reading headers, so the
56    /// `ReadableStream` is never locked.
57    pub fn from_web_sys(req: &web_sys::Request) -> Result<(Self, JsBody), String> {
58        let body = JsBody::new(req.body());
59
60        let method: Method = req
61            .method()
62            .parse()
63            .map_err(|e| format!("invalid method: {e}"))?;
64
65        let uri: Uri = req.url().parse().map_err(|e| format!("invalid URL: {e}"))?;
66
67        // `uri.path()` is the raw, percent-encoded path. Keep it verbatim for
68        // SigV4 signing (the client signs the encoded form), and separately
69        // decode it for operation parsing and bucket/key routing.
70        let signing_path = uri.path().to_string();
71        let path = percent_encoding::percent_decode_str(uri.path())
72            .decode_utf8_lossy()
73            .to_string();
74        let query = uri.query().map(|q| q.to_string());
75        let headers = headermap_from_js(&req.headers());
76
77        Ok((
78            Self {
79                method,
80                path,
81                signing_path,
82                query,
83                headers,
84            },
85            body,
86        ))
87    }
88
89    /// Borrow this struct as a [`RequestInfo`] for gateway dispatch.
90    ///
91    /// Sets the signing path to the raw, percent-encoded
92    /// [`signing_path`](Self::signing_path) so SigV4 verification canonicalizes
93    /// over the path the client actually signed. Without this, a key containing
94    /// a character the client escapes — e.g. a space → `%20` — would be verified
95    /// against the decoded path and fail with `SignatureDoesNotMatch`.
96    pub fn as_request_info(&self) -> RequestInfo<'_> {
97        RequestInfo::new(
98            &self.method,
99            &self.path,
100            self.query.as_deref(),
101            &self.headers,
102            None,
103        )
104        .with_signing_path(&self.signing_path)
105    }
106}