Skip to content

Commit 9880d79

Browse files
committed
provide sync and async versions of parse_multipart
1 parent 40ab44f commit 9880d79

File tree

4 files changed

+141
-56
lines changed

4 files changed

+141
-56
lines changed

libdd-common/src/test_utils.rs

Lines changed: 128 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -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,26 +129,15 @@ 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-
// Extract boundary from Content-Type header
140+
fn extract_multipart_boundary(content_type: &str) -> anyhow::Result<String> {
164141
let mime: mime::Mime = content_type
165142
.parse()
166143
.map_err(|e| anyhow::anyhow!("Failed to parse Content-Type as MIME type: {}", e))?;
@@ -170,15 +147,102 @@ fn parse_multipart(content_type: &str, body: &[u8]) -> anyhow::Result<Vec<Multip
170147
.context("No boundary parameter found in Content-Type")?
171148
.to_string();
172149

173-
// multer is async, which is unnecessary for our use-case so just wrap in block_on to maintain a
174-
// sync API
175-
futures::executor::block_on(parse_multipart_async(boundary, body.to_vec()))
150+
Ok(boundary)
176151
}
177152

178-
async fn parse_multipart_async(
179-
boundary: String,
180-
body: Vec<u8>,
181-
) -> anyhow::Result<Vec<MultipartPart>> {
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)?;
181+
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>> {
182246
use futures_util::stream::once;
183247

184248
let stream = once(async move { Ok::<_, std::io::Error>(bytes::Bytes::from(body)) });
@@ -236,7 +300,7 @@ mod tests {
236300
#[test]
237301
fn test_parse_http_request_basic() {
238302
let request = b"POST /v1/input HTTP/1.1\r\nHost: example.com\r\nContent-Type: application/json\r\n\r\n{\"test\":true}";
239-
let parsed = parse_http_request(request).unwrap();
303+
let parsed = parse_http_request_sync(request).unwrap();
240304

241305
assert_eq!(parsed.method, "POST");
242306
assert_eq!(parsed.path, "/v1/input");
@@ -253,7 +317,7 @@ mod tests {
253317
fn test_parse_http_request_with_custom_headers() {
254318
let request =
255319
b"GET /test HTTP/1.1\r\nX-Custom-Header: value\r\nAnother-Header: 123\r\n\r\n";
256-
let parsed = parse_http_request(request).unwrap();
320+
let parsed = parse_http_request_sync(request).unwrap();
257321

258322
assert_eq!(parsed.method, "GET");
259323
assert_eq!(parsed.path, "/test");
@@ -274,7 +338,26 @@ mod tests {
274338
let mut request_bytes = request.into_bytes();
275339
request_bytes.extend_from_slice(body);
276340

277-
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\nContent-Disposition: form-data; name=\"field\"\r\n\r\nvalue\r\n------WebKitFormBoundary--";
353+
let request = format!(
354+
"POST /v1/input HTTP/1.1\r\nHost: example.com\r\nContent-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();
278361

279362
assert_eq!(parsed.method, "POST");
280363
assert_eq!(parsed.multipart_parts.len(), 1);

libdd-common/tests/reqwest_builder_test.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ mod tests {
4848
let captured = std::fs::read(&*file_path).expect("should read dump file");
4949

5050
// Parse and validate
51-
let request = parse_http_request(&captured).expect("should parse captured request");
51+
let request = parse_http_request(&captured)
52+
.await
53+
.expect("should parse captured request");
5254

5355
assert_eq!(request.method, "POST");
5456
assert_eq!(request.path, "/");

libdd-profiling/tests/exporter_e2e.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
88
mod common;
99

10-
use libdd_common::test_utils::parse_http_request;
10+
use libdd_common::test_utils::{parse_http_request, parse_http_request_sync};
1111
use libdd_profiling::exporter::config;
1212
use libdd_profiling::exporter::{File, ProfileExporter};
1313
use libdd_profiling::internal::EncodedProfile;
@@ -187,7 +187,7 @@ async fn read_and_capture_request<S>(
187187
}
188188
}
189189

190-
if let Ok(req) = parse_http_request(&buffer) {
190+
if let Ok(req) = parse_http_request(&buffer).await {
191191
received_requests.lock().unwrap().push(ReceivedRequest {
192192
method: req.method,
193193
path: req.path,
@@ -277,7 +277,7 @@ async fn export_full_profile(
277277
RequestSource::File(path) => {
278278
// No sleep needed - send_blocking() waits for file to be synced
279279
let request_bytes = std::fs::read(&path)?;
280-
let req = parse_http_request(&request_bytes)?;
280+
let req = parse_http_request(&request_bytes).await?;
281281
Ok(ReceivedRequest {
282282
method: req.method,
283283
path: req.path,
@@ -317,7 +317,7 @@ fn validate_full_export(req: &ReceivedRequest, expected_path: &str) -> anyhow::R
317317
http_request_bytes.extend_from_slice(b"\r\n");
318318
http_request_bytes.extend_from_slice(&req.body);
319319

320-
let parsed_req = parse_http_request(&http_request_bytes)?;
320+
let parsed_req = parse_http_request_sync(&http_request_bytes)?;
321321
let parts = &parsed_req.multipart_parts;
322322

323323
// Find event JSON

libdd-profiling/tests/file_exporter_test.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
mod common;
55

6-
use libdd_common::test_utils::{create_temp_file_path, parse_http_request, TempFileGuard};
6+
use libdd_common::test_utils::{create_temp_file_path, parse_http_request_sync, TempFileGuard};
77
use libdd_profiling::exporter::ProfileExporter;
88
use libdd_profiling::internal::EncodedProfile;
99

@@ -72,7 +72,7 @@ mod tests {
7272
let request_bytes = std::fs::read(&file_path).expect("read dump file");
7373

7474
// Parse HTTP request
75-
let request = parse_http_request(&request_bytes).expect("parse HTTP request");
75+
let request = parse_http_request_sync(&request_bytes).expect("parse HTTP request");
7676

7777
// Validate request line
7878
assert_eq!(request.method, "POST");
@@ -182,7 +182,7 @@ mod tests {
182182
let request_bytes = std::fs::read(&file_path).expect("read dump file");
183183

184184
// Parse and validate
185-
let request = parse_http_request(&request_bytes).expect("parse HTTP request");
185+
let request = parse_http_request_sync(&request_bytes).expect("parse HTTP request");
186186
let event_part = request
187187
.multipart_parts
188188
.iter()
@@ -231,7 +231,7 @@ mod tests {
231231
let request_bytes = std::fs::read(&file_path).expect("read dump file");
232232

233233
// Parse and validate
234-
let request = parse_http_request(&request_bytes).expect("parse HTTP request");
234+
let request = parse_http_request_sync(&request_bytes).expect("parse HTTP request");
235235
let event_part = request
236236
.multipart_parts
237237
.iter()
@@ -287,7 +287,7 @@ mod tests {
287287
let request_bytes = std::fs::read(&file_path).expect("read dump file");
288288

289289
// Parse and validate
290-
let request = parse_http_request(&request_bytes).expect("parse HTTP request");
290+
let request = parse_http_request_sync(&request_bytes).expect("parse HTTP request");
291291
let event_part = request
292292
.multipart_parts
293293
.iter()
@@ -327,7 +327,7 @@ mod tests {
327327
let request_bytes = std::fs::read(&file_path).expect("read dump file");
328328

329329
// Parse HTTP request
330-
let request = parse_http_request(&request_bytes).expect("parse HTTP request");
330+
let request = parse_http_request_sync(&request_bytes).expect("parse HTTP request");
331331

332332
// Validate headers - API key should be present
333333
assert_eq!(request.headers.get("dd-api-key").unwrap(), api_key);

0 commit comments

Comments
 (0)