Skip to content

Commit 2ef169f

Browse files
authored
Merge 9880d79 into 0411c94
2 parents 0411c94 + 9880d79 commit 2ef169f

File tree

8 files changed

+612
-2251
lines changed

8 files changed

+612
-2251
lines changed

Cargo.lock

Lines changed: 128 additions & 602 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

LICENSE-3rdparty.yml

Lines changed: 325 additions & 1580 deletions
Large diffs are not rendered by default.

libdd-common/Cargo.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ http-body-util = "0.1"
3030
tower-service = "0.3"
3131
cc = "1.1.31"
3232
mime = { version = "0.3.16", optional = true }
33-
multipart = { version = "0.18", optional = true }
33+
multer = { version = "3.1", optional = true }
34+
bytes = { version = "1.4", optional = true }
3435
pin-project = "1"
3536
rand = { version = "0.8", optional = true }
3637
regex = "1.5"
@@ -71,7 +72,8 @@ httparse = "1.9"
7172
indexmap = "2.11"
7273
maplit = "1.0"
7374
mime = "0.3.16"
74-
multipart = "0.18"
75+
multer = "3.1"
76+
bytes = "1.4"
7577
rand = "0.8"
7678
tempfile = "3.8"
7779
tokio = { version = "1.23", features = ["rt", "macros", "time"] }
@@ -88,7 +90,7 @@ fips = ["https", "hyper-rustls/fips"]
8890
# Enable reqwest client builder support with file dump debugging
8991
reqwest = ["dep:reqwest", "test-utils"]
9092
# Enable test utilities for use in other crates
91-
test-utils = ["dep:httparse", "dep:rand", "dep:mime", "dep:multipart"]
93+
test-utils = ["dep:httparse", "dep:rand", "dep:mime", "dep:multer", "dep:bytes"]
9294

9395
[lints.rust]
9496
# We run coverage checks in our github actions. These checks are run with

libdd-common/src/test_utils.rs

Lines changed: 140 additions & 53 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,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\nHost: example.com\r\nContent-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\nX-Custom-Header: value\r\nAnother-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\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();
274361

275362
assert_eq!(parsed.method, "POST");
276363
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/Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ bench = false
1919
[features]
2020
default = []
2121
cxx = ["dep:cxx", "dep:cxx-build"]
22-
test-utils = ["dep:multipart"]
22+
test-utils = ["libdd-common/test-utils"]
2323

2424
[[bench]]
2525
name = "main"
@@ -45,7 +45,6 @@ libdd-alloc = { version = "1.0.0", path = "../libdd-alloc" }
4545
libdd-common = { version = "1.1.0", path = "../libdd-common", default-features = false, features = ["reqwest", "test-utils"] }
4646
libdd-profiling-protobuf = { version = "1.0.0", path = "../libdd-profiling-protobuf", features = ["prost_impls"] }
4747
mime = "0.3.16"
48-
multipart = { version = "0.18", optional = true }
4948
parking_lot = { version = "0.12", default-features = false }
5049
prost = "0.14.1"
5150
rand = "0.8"

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)