Skip to content

Commit 573fa04

Browse files
committed
Implement HTTP layer streaming for large file uploads
- Add RequestBody enum with Memory and File variants for efficient handling - Implement automatic streaming logic: small uploads in memory, large to disk - Update upload handlers to work with new RequestBody structure - Add comprehensive HTTP streaming tests - Update all test files to use new RequestBody API - Fix clippy warnings and maintain code quality - Achieve scalability and resource protection for large uploads
1 parent 980b637 commit 573fa04

13 files changed

Lines changed: 478 additions & 132 deletions

src/http.rs

Lines changed: 162 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::response::create_error_response;
66
use crate::router::Router;
77
use log::{debug, error, info, warn};
88
use std::collections::HashMap;
9+
use std::fs::File;
910
use std::io::prelude::*;
1011
use std::net::TcpStream;
1112
use std::path::PathBuf;
@@ -17,13 +18,40 @@ const MAX_REQUEST_BODY_SIZE: usize = 10 * 1024 * 1024 * 1024;
1718
/// Maximum size for request headers (8KB) to prevent header buffer overflow
1819
const MAX_HEADERS_SIZE: usize = 8 * 1024;
1920

21+
/// Threshold for streaming request bodies to disk (128MB)
22+
pub const STREAM_TO_DISK_THRESHOLD: usize = 128 * 1024 * 1024;
23+
2024
/// Represents a parsed incoming HTTP request.
2125
#[derive(Debug)]
2226
pub struct Request {
2327
pub method: String,
2428
pub path: String,
2529
pub headers: HashMap<String, String>,
26-
pub body: Option<Vec<u8>>,
30+
pub body: Option<RequestBody>,
31+
}
32+
33+
/// Request body can be either in memory or streamed to disk for large uploads
34+
#[derive(Debug)]
35+
pub enum RequestBody {
36+
/// Small bodies stored in memory
37+
Memory(Vec<u8>),
38+
/// Large bodies streamed to temporary file
39+
File { path: PathBuf, size: u64 },
40+
}
41+
42+
impl RequestBody {
43+
/// Get the size of the request body in bytes
44+
pub fn len(&self) -> usize {
45+
match self {
46+
RequestBody::Memory(data) => data.len(),
47+
RequestBody::File { size, .. } => *size as usize,
48+
}
49+
}
50+
51+
/// Check if the request body is empty
52+
pub fn is_empty(&self) -> bool {
53+
self.len() == 0
54+
}
2755
}
2856

2957
/// Represents an outgoing HTTP response.
@@ -182,11 +210,12 @@ impl Request {
182210
}
183211

184212
/// Read request body based on Content-Length header with security validations
213+
/// Large bodies are streamed to disk to prevent memory exhaustion
185214
fn read_request_body(
186215
stream: &mut TcpStream,
187216
headers: &HashMap<String, String>,
188217
remaining_bytes: Vec<u8>,
189-
) -> Result<Option<Vec<u8>>, AppError> {
218+
) -> Result<Option<RequestBody>, AppError> {
190219
// Check if we have a Content-Length header
191220
let content_length = match headers.get("content-length") {
192221
Some(length_str) => match length_str.parse::<usize>() {
@@ -208,13 +237,31 @@ impl Request {
208237

209238
// Validate content length against security limits
210239
if content_length == 0 {
211-
return Ok(Some(Vec::new()));
240+
return Ok(Some(RequestBody::Memory(Vec::new())));
212241
}
213242

214243
if content_length > MAX_REQUEST_BODY_SIZE {
215244
return Err(AppError::PayloadTooLarge(MAX_REQUEST_BODY_SIZE as u64));
216245
}
217246

247+
// Decide whether to use memory or disk based on size
248+
if content_length <= STREAM_TO_DISK_THRESHOLD {
249+
// Small body - use memory
250+
Self::read_body_to_memory(stream, content_length, remaining_bytes)
251+
.map(|body| Some(RequestBody::Memory(body)))
252+
} else {
253+
// Large body - stream to disk
254+
Self::read_body_to_disk(stream, content_length, remaining_bytes)
255+
.map(|(path, size)| Some(RequestBody::File { path, size }))
256+
}
257+
}
258+
259+
/// Read small request body into memory
260+
fn read_body_to_memory(
261+
stream: &mut TcpStream,
262+
content_length: usize,
263+
remaining_bytes: Vec<u8>,
264+
) -> Result<Vec<u8>, AppError> {
218265
let mut body = Vec::with_capacity(content_length);
219266

220267
// Use any remaining bytes from header parsing
@@ -225,7 +272,7 @@ impl Request {
225272
let bytes_needed = content_length - bytes_from_headers;
226273

227274
if bytes_needed > 0 {
228-
// Read the remaining body in chunks to avoid large allocations
275+
// Read the remaining body in chunks
229276
let mut bytes_read = 0;
230277
let chunk_size = 8192; // 8KB chunks
231278
let mut buffer = vec![0; chunk_size];
@@ -235,7 +282,6 @@ impl Request {
235282

236283
match stream.read(&mut buffer[..to_read]) {
237284
Ok(0) => {
238-
// Unexpected end of stream
239285
return Err(AppError::BadRequest);
240286
}
241287
Ok(n) => {
@@ -257,8 +303,103 @@ impl Request {
257303
return Err(AppError::BadRequest);
258304
}
259305

260-
debug!("Successfully read request body: {} bytes", body.len());
261-
Ok(Some(body))
306+
debug!(
307+
"Successfully read request body to memory: {} bytes",
308+
body.len()
309+
);
310+
Ok(body)
311+
}
312+
313+
/// Read large request body directly to disk to prevent memory exhaustion
314+
fn read_body_to_disk(
315+
stream: &mut TcpStream,
316+
content_length: usize,
317+
remaining_bytes: Vec<u8>,
318+
) -> Result<(PathBuf, u64), AppError> {
319+
// Create temporary file for the request body
320+
let temp_filename = format!(
321+
"irondrop_request_{}_{:x}.tmp",
322+
std::process::id(),
323+
std::time::SystemTime::now()
324+
.duration_since(std::time::UNIX_EPOCH)
325+
.unwrap_or_default()
326+
.as_nanos()
327+
);
328+
329+
// Use system temp directory
330+
let temp_dir = std::env::temp_dir();
331+
let temp_path = temp_dir.join(&temp_filename);
332+
333+
let mut temp_file = File::create(&temp_path).map_err(|e| {
334+
error!("Failed to create temporary file {temp_path:?}: {e}");
335+
AppError::from(e)
336+
})?;
337+
338+
let mut total_written = 0;
339+
340+
// Write any remaining bytes from header parsing
341+
if !remaining_bytes.is_empty() {
342+
let bytes_to_write = remaining_bytes.len().min(content_length);
343+
temp_file
344+
.write_all(&remaining_bytes[..bytes_to_write])
345+
.map_err(|e| {
346+
let _ = std::fs::remove_file(&temp_path);
347+
AppError::from(e)
348+
})?;
349+
total_written += bytes_to_write;
350+
}
351+
352+
// Stream remaining bytes directly to disk
353+
let bytes_needed = content_length - total_written;
354+
if bytes_needed > 0 {
355+
let mut bytes_read = 0;
356+
let chunk_size = 64 * 1024; // 64KB chunks for better disk I/O
357+
let mut buffer = vec![0; chunk_size];
358+
359+
while bytes_read < bytes_needed {
360+
let to_read = (bytes_needed - bytes_read).min(chunk_size);
361+
362+
match stream.read(&mut buffer[..to_read]) {
363+
Ok(0) => {
364+
let _ = std::fs::remove_file(&temp_path);
365+
return Err(AppError::BadRequest);
366+
}
367+
Ok(n) => {
368+
temp_file.write_all(&buffer[..n]).map_err(|e| {
369+
let _ = std::fs::remove_file(&temp_path);
370+
AppError::from(e)
371+
})?;
372+
bytes_read += n;
373+
total_written += n;
374+
}
375+
Err(e) => {
376+
let _ = std::fs::remove_file(&temp_path);
377+
if e.kind() == std::io::ErrorKind::TimedOut {
378+
warn!("Request body read timeout");
379+
}
380+
return Err(AppError::Io(e));
381+
}
382+
}
383+
}
384+
}
385+
386+
// Ensure all data is written to disk
387+
temp_file.sync_all().map_err(|e| {
388+
let _ = std::fs::remove_file(&temp_path);
389+
AppError::from(e)
390+
})?;
391+
392+
// Verify we read exactly the expected amount
393+
if total_written != content_length {
394+
let _ = std::fs::remove_file(&temp_path);
395+
return Err(AppError::BadRequest);
396+
}
397+
398+
debug!(
399+
"Successfully streamed request body to disk: {} bytes at {temp_path:?}",
400+
total_written
401+
);
402+
Ok((temp_path, total_written as u64))
262403
}
263404

264405
/// Simple URL decoding for percent-encoded paths
@@ -294,6 +435,17 @@ impl Request {
294435

295436
Ok(decoded)
296437
}
438+
439+
/// Clean up any temporary files associated with this request
440+
pub fn cleanup(&self) {
441+
if let Some(RequestBody::File { path, .. }) = &self.body {
442+
if let Err(e) = std::fs::remove_file(path) {
443+
warn!("Failed to clean up temporary file {path:?}: {e}");
444+
} else {
445+
debug!("Cleaned up temporary file: {path:?}");
446+
}
447+
}
448+
}
297449
}
298450

299451
/// Top-level function to handle a client connection.
@@ -354,6 +506,9 @@ pub fn handle_client(
354506
}
355507
}
356508
}
509+
510+
// Clean up any temporary files created during request processing
511+
request.cleanup();
357512
}
358513

359514
// Static asset, favicon, upload, and health handlers moved to handlers.rs

src/multipart.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,14 @@
7878
//! ..Default::default()
7979
//! };
8080
//!
81-
//! let parser = MultipartParser::new(Cursor::new(body.clone()), &boundary, config)?;
81+
//! // Create parser (example with memory body)
82+
//! let data = match body {
83+
//! irondrop::http::RequestBody::Memory(data) => data.clone(),
84+
//! irondrop::http::RequestBody::File { path, .. } => {
85+
//! std::fs::read(path)?
86+
//! }
87+
//! };
88+
//! let parser = MultipartParser::new(Cursor::new(data), &boundary, config)?;
8289
//!
8390
//! // Process each part
8491
//! for part_result in parser {

0 commit comments

Comments
 (0)