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