Skip to main content

rust_webx_host/
context.rs

1//! HttpContext —Runtime implementation of IHttpContext, IHttpRequest, IHttpResponse.
2
3use http_body_util::Full;
4use hyper::body::{Bytes, Incoming};
5use hyper::Request;
6use rust_webx_core::auth::IClaims;
7use rust_webx_core::error::Result;
8use rust_webx_core::http::{IClaimsExt, IHttpContext, IHttpRequest, IHttpResponse};
9
10use crate::problem_response::{build_problem, problem_to_bytes};
11use std::cell::RefCell;
12use std::collections::HashMap;
13
14/// Concrete implementation of IHttpContext wrapping a hyper request and response.
15///
16/// The request body is eagerly read during construction so that
17/// the IHttpRequest trait can provide sync access to body bytes.
18pub struct HttpContext {
19    req: HttpRequest,
20    resp: HttpResponse,
21    /// Authentication claims set by authentication middleware.
22    claims: RefCell<Option<Box<dyn IClaims>>>,
23}
24
25impl HttpContext {
26    /// Create a new HttpContext by eagerly reading the request body.
27    ///
28    /// Reads the body in chunks using [`BodyExt::frame`], tracking total size.
29    /// When `max_body_size` is exceeded, the context is built with a 413
30    /// Payload Too Large response pre-set.
31    pub async fn new(req: Request<Incoming>, max_body_size: usize) -> Self {
32        use http_body_util::BodyExt;
33
34        let (parts, mut body) = req.into_parts();
35        let method = parts.method.to_string();
36        let path = parts.uri.path().to_string();
37
38        // Read headers
39        let mut headers = HashMap::new();
40        for (name, value) in parts.headers.iter() {
41            if let Ok(v) = value.to_str() {
42                headers.insert(name.to_string(), v.to_string());
43            }
44        }
45
46        // Parse query string
47        let query_params = parts
48            .uri
49            .query()
50            .map(parse_query_string)
51            .unwrap_or_default();
52
53        // Read body in chunks, tracking total size
54        let mut body_bytes = Vec::new();
55        let mut total_size: usize = 0;
56        let mut payload_too_large = false;
57
58        while let Some(frame_result) = body.frame().await {
59            match frame_result {
60                Ok(frame) => {
61                    if let Some(data) = frame.data_ref() {
62                        total_size = total_size.saturating_add(data.len());
63                        if total_size > max_body_size {
64                            payload_too_large = true;
65                            break;
66                        }
67                        body_bytes.extend_from_slice(data);
68                    }
69                }
70                Err(_) => {
71                    break;
72                }
73            }
74        }
75
76        let req = HttpRequest {
77            method,
78            path,
79            headers,
80            query_params,
81            route_params: HashMap::new(),
82            route_pattern: None,
83            body_bytes,
84        };
85
86        let mut resp = HttpResponse::new(200);
87
88        if payload_too_large {
89            resp.set_status(413);
90            let problem = build_problem(
91                413,
92                "Request body exceeds maximum allowed size",
93            );
94            resp.body = Some(problem_to_bytes(&problem));
95            resp.headers.insert(
96                "content-type".to_string(),
97                "application/problem+json".to_string(),
98            );
99        }
100
101        Self {
102            req,
103            resp,
104            claims: RefCell::new(None),
105        }
106    }
107
108    pub fn into_response(self) -> hyper::Response<Full<Bytes>> {
109        self.resp.into_hyper()
110    }
111}
112
113impl IClaimsExt for HttpContext {
114    fn set_claims(&mut self, claims: Box<dyn IClaims>) {
115        *self.claims.borrow_mut() = Some(claims);
116    }
117
118    fn claims(&self) -> Option<&dyn IClaims> {
119        let borrowed = self.claims.borrow();
120        // SAFETY: We extend the Ref's lifetime to match &self. This is sound
121        // because set_claims requires &mut self and can't be called concurrently
122        // with any &self method, so the RefCell's contents are stable for the
123        // duration of &self.
124        borrowed
125            .as_ref()
126            .map(|b| unsafe { &*(&**b as *const dyn IClaims) })
127    }
128}
129
130impl IHttpContext for HttpContext {
131    fn request(&self) -> &dyn IHttpRequest {
132        &self.req
133    }
134
135    fn request_mut(&mut self) -> &mut dyn IHttpRequest {
136        &mut self.req
137    }
138
139    fn response(&self) -> &dyn IHttpResponse {
140        &self.resp
141    }
142
143    fn response_mut(&mut self) -> &mut dyn IHttpResponse {
144        &mut self.resp
145    }
146}
147
148/// Concrete implementation of IHttpRequest.
149///
150/// Body bytes are stored eagerly so that IHttpRequest can be dyn-compatible
151/// without requiring `&mut self` for async body reading.
152pub struct HttpRequest {
153    method: String,
154    path: String,
155    headers: HashMap<String, String>,
156    query_params: HashMap<String, String>,
157    route_params: HashMap<String, String>,
158    route_pattern: Option<String>,
159    body_bytes: Vec<u8>,
160}
161
162#[async_trait::async_trait]
163impl IHttpRequest for HttpRequest {
164    fn method(&self) -> &str {
165        &self.method
166    }
167
168    fn path(&self) -> &str {
169        &self.path
170    }
171
172    fn header(&self, name: &str) -> Option<&str> {
173        self.headers.get(name).map(|s| s.as_str())
174    }
175
176    fn query(&self) -> &HashMap<String, String> {
177        &self.query_params
178    }
179
180    fn route_params(&self) -> &HashMap<String, String> {
181        &self.route_params
182    }
183
184    fn route_params_mut(&mut self) -> &mut HashMap<String, String> {
185        &mut self.route_params
186    }
187
188    fn route_pattern(&self) -> Option<&str> {
189        self.route_pattern.as_deref()
190    }
191
192    fn route_pattern_mut(&mut self) -> &mut Option<String> {
193        &mut self.route_pattern
194    }
195
196    async fn body_bytes(&self) -> Result<Vec<u8>> {
197        Ok(self.body_bytes.clone())
198    }
199
200    async fn body_text(&self) -> Result<String> {
201        String::from_utf8(self.body_bytes.clone())
202            .map_err(|e| rust_webx_core::error::Error::Http(e.to_string()))
203    }
204}
205
206/// Concrete implementation of IHttpResponse.
207pub struct HttpResponse {
208    status: u16,
209    headers: HashMap<String, String>,
210    body: Option<Vec<u8>>,
211}
212
213impl HttpResponse {
214    pub fn new(status: u16) -> Self {
215        let mut headers = HashMap::new();
216        headers.insert("content-type".to_string(), "application/json".to_string());
217        Self {
218            status,
219            headers,
220            body: None,
221        }
222    }
223
224    pub fn into_hyper(self) -> hyper::Response<Full<Bytes>> {
225        let mut builder = hyper::Response::builder().status(self.status);
226        for (key, value) in &self.headers {
227            builder = builder.header(key.as_str(), value.as_str());
228        }
229        let body_bytes = self.body.unwrap_or_default();
230        builder
231            .body(Full::new(Bytes::from(body_bytes)))
232            .unwrap_or_else(|_| {
233                hyper::Response::builder()
234                    .status(hyper::StatusCode::INTERNAL_SERVER_ERROR)
235                    .body(Full::new(Bytes::from("Internal Server Error")))
236                    .unwrap()
237            })
238    }
239}
240
241#[async_trait::async_trait]
242impl IHttpResponse for HttpResponse {
243    fn status(&self) -> u16 {
244        self.status
245    }
246
247    fn has_body(&self) -> bool {
248        self.body.is_some()
249    }
250
251    fn set_status(&mut self, code: u16) {
252        self.status = code;
253    }
254
255    fn set_header(&mut self, key: &str, value: &str) {
256        self.headers.insert(key.to_string(), value.to_string());
257    }
258
259    async fn write_bytes(&mut self, data: Vec<u8>) -> Result<()> {
260        self.body = Some(data);
261        Ok(())
262    }
263
264    async fn write_text(&mut self, text: &str) -> Result<()> {
265        self.body = Some(text.as_bytes().to_vec());
266        Ok(())
267    }
268
269    fn body_bytes(&self) -> Vec<u8> {
270        self.body.clone().unwrap_or_default()
271    }
272
273    fn header(&self, key: &str) -> Option<&str> {
274        self.headers.get(key).map(|s| s.as_str())
275    }
276}
277
278/// Parse a query string like "key1=val1&key2=val2" into a HashMap.
279fn parse_query_string(query: &str) -> HashMap<String, String> {
280    let mut params = HashMap::new();
281    for pair in query.split('&') {
282        let mut parts = pair.splitn(2, '=');
283        if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
284            params.insert(percent_decode(key), percent_decode(value));
285        }
286    }
287    params
288}
289
290fn percent_decode(s: &str) -> String {
291    let mut result = String::with_capacity(s.len());
292    let mut chars = s.chars();
293    while let Some(c) = chars.next() {
294        if c == '%' {
295            let hex: String = chars.by_ref().take(2).collect();
296            if let Ok(byte) = u8::from_str_radix(&hex, 16) {
297                result.push(byte as char);
298            }
299        } else if c == '+' {
300            result.push(' ');
301        } else {
302            result.push(c);
303        }
304    }
305    result
306}