@@ -99,27 +99,16 @@ pub struct MultipartPart {
9999 pub content : Vec < u8 > ,
100100}
101101
102- /// Parse an HTTP request from raw bytes
103- ///
104- /// If the Content-Type header indicates multipart/form-data, the multipart body will be
105- /// automatically parsed and available in the `multipart_parts` field.
106- ///
107- /// # Arguments
108- /// * `data` - Raw HTTP request bytes including headers and body
109- ///
110- /// # Returns
111- /// A parsed `HttpRequest` or an error if parsing fails
112- ///
113- /// # Example
114- /// ```no_run
115- /// use libdd_common::test_utils::parse_http_request;
116- ///
117- /// let request_bytes = b"POST /v1/input HTTP/1.1\r\nHost: example.com\r\n\r\nbody";
118- /// let request = parse_http_request(request_bytes).unwrap();
119- /// assert_eq!(request.method, "POST");
120- /// assert_eq!(request.path, "/v1/input");
121- /// ```
122- pub fn parse_http_request ( data : & [ u8 ] ) -> anyhow:: Result < HttpRequest > {
102+ /// Parsed HTTP request components without multipart parsing.
103+ /// This is the shared result from `parse_http_request_headers`.
104+ struct ParsedRequestParts {
105+ method : String ,
106+ path : String ,
107+ headers : HashMap < String , String > ,
108+ body : Vec < u8 > ,
109+ }
110+
111+ fn parse_http_request_headers ( data : & [ u8 ] ) -> anyhow:: Result < ParsedRequestParts > {
123112 let mut header_buf = [ httparse:: EMPTY_HEADER ; 64 ] ;
124113 let mut req = httparse:: Request :: new ( & mut header_buf) ;
125114
@@ -131,7 +120,6 @@ pub fn parse_http_request(data: &[u8]) -> anyhow::Result<HttpRequest> {
131120 let method = req. method . context ( "No method found" ) ?. to_string ( ) ;
132121 let path = req. path . context ( "No path found" ) ?. to_string ( ) ;
133122
134- // Convert headers to HashMap with lowercase keys
135123 let mut headers = HashMap :: new ( ) ;
136124 for header in req. headers {
137125 let key = header. name . to_lowercase ( ) ;
@@ -141,51 +129,131 @@ pub fn parse_http_request(data: &[u8]) -> anyhow::Result<HttpRequest> {
141129
142130 let body = data[ headers_len..] . to_vec ( ) ;
143131
144- // Auto-parse multipart if Content-Type indicates multipart/form-data
145- let multipart_parts = match headers. get ( "content-type" ) {
146- Some ( ct) if ct. contains ( "multipart/form-data" ) => parse_multipart ( ct, & body) ?,
147- _ => Vec :: new ( ) ,
148- } ;
149-
150- Ok ( HttpRequest {
132+ Ok ( ParsedRequestParts {
151133 method,
152134 path,
153135 headers,
154136 body,
155- multipart_parts,
156137 } )
157138}
158139
159- /// Parse multipart form data from Content-Type header and body
160- ///
161- /// Extracts the boundary from the Content-Type header and parses the multipart body.
162- fn parse_multipart ( content_type : & str , body : & [ u8 ] ) -> anyhow:: Result < Vec < MultipartPart > > {
163- use multipart:: server:: Multipart ;
164- use std:: io:: Cursor ;
165-
166- // Extract boundary from Content-Type header
140+ fn extract_multipart_boundary ( content_type : & str ) -> anyhow:: Result < String > {
167141 let mime: mime:: Mime = content_type
168142 . parse ( )
169143 . map_err ( |e| anyhow:: anyhow!( "Failed to parse Content-Type as MIME type: {}" , e) ) ?;
170144
171145 let boundary = mime
172146 . get_param ( mime:: BOUNDARY )
173147 . context ( "No boundary parameter found in Content-Type" ) ?
174- . as_str ( ) ;
148+ . to_string ( ) ;
175149
176- // Parse multipart body
177- let cursor = Cursor :: new ( body) ;
178- let mut multipart = Multipart :: with_body ( cursor, boundary) ;
179- let mut parts = Vec :: new ( ) ;
150+ Ok ( boundary)
151+ }
180152
181- while let Some ( mut field) = multipart. read_entry ( ) ? {
182- let headers = & field. headers ;
183- let name = headers. name . to_string ( ) ;
184- let filename = headers. filename . clone ( ) ;
185- let content_type = headers. content_type . as_ref ( ) . map ( |ct| ct. to_string ( ) ) ;
153+ /// Parse an HTTP request from raw bytes (async version).
154+ ///
155+ /// If the Content-Type header indicates multipart/form-data, the multipart body will be
156+ /// automatically parsed and available in the `multipart_parts` field.
157+ ///
158+ /// Use this function in async contexts (e.g., `#[tokio::test]`). For synchronous contexts,
159+ /// use [`parse_http_request_sync`] instead.
160+ ///
161+ /// # Arguments
162+ /// * `data` - Raw HTTP request bytes including headers and body
163+ ///
164+ /// # Returns
165+ /// A parsed `HttpRequest` or an error if parsing fails
166+ ///
167+ /// # Example
168+ /// ```no_run
169+ /// use libdd_common::test_utils::parse_http_request;
170+ ///
171+ /// # async fn example() -> anyhow::Result<()> {
172+ /// let request_bytes = b"POST /v1/input HTTP/1.1\r\nHost: example.com\r\n\r\nbody";
173+ /// let request = parse_http_request(request_bytes).await?;
174+ /// assert_eq!(request.method, "POST");
175+ /// assert_eq!(request.path, "/v1/input");
176+ /// # Ok(())
177+ /// # }
178+ /// ```
179+ pub async fn parse_http_request ( data : & [ u8 ] ) -> anyhow:: Result < HttpRequest > {
180+ let parts = parse_http_request_headers ( data) ?;
186181
187- let mut content = Vec :: new ( ) ;
188- std:: io:: Read :: read_to_end ( & mut field. data , & mut content) ?;
182+ // Auto-parse multipart if Content-Type indicates multipart/form-data
183+ let multipart_parts = match parts. headers . get ( "content-type" ) {
184+ Some ( ct) if ct. contains ( "multipart/form-data" ) => {
185+ let boundary = extract_multipart_boundary ( ct) ?;
186+ parse_multipart ( boundary, parts. body . clone ( ) ) . await ?
187+ }
188+ _ => Vec :: new ( ) ,
189+ } ;
190+
191+ Ok ( HttpRequest {
192+ method : parts. method ,
193+ path : parts. path ,
194+ headers : parts. headers ,
195+ body : parts. body ,
196+ multipart_parts,
197+ } )
198+ }
199+
200+ /// Parse an HTTP request from raw bytes (sync version).
201+ ///
202+ /// If the Content-Type header indicates multipart/form-data, the multipart body will be
203+ /// automatically parsed and available in the `multipart_parts` field.
204+ ///
205+ /// **Note:** This function uses `futures::executor::block_on` internally for multipart parsing.
206+ /// In async contexts (e.g., `#[tokio::test]`), prefer [`parse_http_request`] to avoid blocking
207+ /// the async runtime.
208+ ///
209+ /// # Arguments
210+ /// * `data` - Raw HTTP request bytes including headers and body
211+ ///
212+ /// # Returns
213+ /// A parsed `HttpRequest` or an error if parsing fails
214+ ///
215+ /// # Example
216+ /// ```no_run
217+ /// use libdd_common::test_utils::parse_http_request_sync;
218+ ///
219+ /// let request_bytes = b"POST /v1/input HTTP/1.1\r\nHost: example.com\r\n\r\nbody";
220+ /// let request = parse_http_request_sync(request_bytes).unwrap();
221+ /// assert_eq!(request.method, "POST");
222+ /// assert_eq!(request.path, "/v1/input");
223+ /// ```
224+ pub fn parse_http_request_sync ( data : & [ u8 ] ) -> anyhow:: Result < HttpRequest > {
225+ let parts = parse_http_request_headers ( data) ?;
226+
227+ // Auto-parse multipart if Content-Type indicates multipart/form-data
228+ let multipart_parts = match parts. headers . get ( "content-type" ) {
229+ Some ( ct) if ct. contains ( "multipart/form-data" ) => {
230+ let boundary = extract_multipart_boundary ( ct) ?;
231+ futures:: executor:: block_on ( parse_multipart ( boundary, parts. body . clone ( ) ) ) ?
232+ }
233+ _ => Vec :: new ( ) ,
234+ } ;
235+
236+ Ok ( HttpRequest {
237+ method : parts. method ,
238+ path : parts. path ,
239+ headers : parts. headers ,
240+ body : parts. body ,
241+ multipart_parts,
242+ } )
243+ }
244+
245+ async fn parse_multipart ( boundary : String , body : Vec < u8 > ) -> anyhow:: Result < Vec < MultipartPart > > {
246+ use futures_util:: stream:: once;
247+
248+ let stream = once ( async move { Ok :: < _ , std:: io:: Error > ( bytes:: Bytes :: from ( body) ) } ) ;
249+ let mut multipart = multer:: Multipart :: new ( stream, boundary) ;
250+ let mut parts = Vec :: new ( ) ;
251+
252+ while let Some ( field) = multipart. next_field ( ) . await ? {
253+ let name = field. name ( ) . unwrap_or_default ( ) . to_string ( ) ;
254+ let filename = field. file_name ( ) . map ( |s| s. to_string ( ) ) ;
255+ let content_type = field. content_type ( ) . map ( |m| m. to_string ( ) ) ;
256+ let content = field. bytes ( ) . await ?. to_vec ( ) ;
189257
190258 parts. push ( MultipartPart {
191259 name,
@@ -232,7 +300,7 @@ mod tests {
232300 #[ test]
233301 fn test_parse_http_request_basic ( ) {
234302 let request = b"POST /v1/input HTTP/1.1\r \n Host: example.com\r \n Content-Type: application/json\r \n \r \n {\" test\" :true}" ;
235- let parsed = parse_http_request ( request) . unwrap ( ) ;
303+ let parsed = parse_http_request_sync ( request) . unwrap ( ) ;
236304
237305 assert_eq ! ( parsed. method, "POST" ) ;
238306 assert_eq ! ( parsed. path, "/v1/input" ) ;
@@ -249,7 +317,7 @@ mod tests {
249317 fn test_parse_http_request_with_custom_headers ( ) {
250318 let request =
251319 b"GET /test HTTP/1.1\r \n X-Custom-Header: value\r \n Another-Header: 123\r \n \r \n " ;
252- let parsed = parse_http_request ( request) . unwrap ( ) ;
320+ let parsed = parse_http_request_sync ( request) . unwrap ( ) ;
253321
254322 assert_eq ! ( parsed. method, "GET" ) ;
255323 assert_eq ! ( parsed. path, "/test" ) ;
@@ -270,7 +338,26 @@ mod tests {
270338 let mut request_bytes = request. into_bytes ( ) ;
271339 request_bytes. extend_from_slice ( body) ;
272340
273- let parsed = parse_http_request ( & request_bytes) . unwrap ( ) ;
341+ let parsed = parse_http_request_sync ( & request_bytes) . unwrap ( ) ;
342+
343+ assert_eq ! ( parsed. method, "POST" ) ;
344+ assert_eq ! ( parsed. multipart_parts. len( ) , 1 ) ;
345+ assert_eq ! ( parsed. multipart_parts[ 0 ] . name, "field" ) ;
346+ assert_eq ! ( parsed. multipart_parts[ 0 ] . content, b"value" ) ;
347+ }
348+
349+ #[ tokio:: test]
350+ async fn test_parse_http_request_async_with_multipart ( ) {
351+ let content_type = "multipart/form-data; boundary=----WebKitFormBoundary" ;
352+ let body = b"------WebKitFormBoundary\r \n Content-Disposition: form-data; name=\" field\" \r \n \r \n value\r \n ------WebKitFormBoundary--" ;
353+ let request = format ! (
354+ "POST /v1/input HTTP/1.1\r \n Host: example.com\r \n Content-Type: {}\r \n \r \n " ,
355+ content_type
356+ ) ;
357+ let mut request_bytes = request. into_bytes ( ) ;
358+ request_bytes. extend_from_slice ( body) ;
359+
360+ let parsed = parse_http_request ( & request_bytes) . await . unwrap ( ) ;
274361
275362 assert_eq ! ( parsed. method, "POST" ) ;
276363 assert_eq ! ( parsed. multipart_parts. len( ) , 1 ) ;
0 commit comments