diff --git a/seaweed-volume/src/server/handlers.rs b/seaweed-volume/src/server/handlers.rs index 12ec20bad..08c6046f9 100644 --- a/seaweed-volume/src/server/handlers.rs +++ b/seaweed-volume/src/server/handlers.rs @@ -1320,95 +1320,121 @@ async fn get_or_head_handler_inner( } /// Handle HTTP Range requests. Returns 206 Partial Content or 416 Range Not Satisfiable. +#[derive(Clone, Copy)] +struct HttpRange { + start: i64, + length: i64, +} + +fn parse_range_header(s: &str, size: i64) -> Result, &'static str> { + if s.is_empty() { + return Ok(Vec::new()); + } + const PREFIX: &str = "bytes="; + if !s.starts_with(PREFIX) { + return Err("invalid range"); + } + let mut ranges = Vec::new(); + for part in s[PREFIX.len()..].split(',') { + let part = part.trim(); + if part.is_empty() { + continue; + } + let Some(pos) = part.find('-') else { + return Err("invalid range"); + }; + let start_str = part[..pos].trim(); + let end_str = part[pos + 1..].trim(); + let mut r = HttpRange { start: 0, length: 0 }; + if start_str.is_empty() { + let mut i = end_str.parse::().map_err(|_| "invalid range")?; + if i > size { + i = size; + } + r.start = size - i; + r.length = size - r.start; + } else { + let i = start_str.parse::().map_err(|_| "invalid range")?; + if i > size || i < 0 { + return Err("invalid range"); + } + r.start = i; + if end_str.is_empty() { + r.length = size - r.start; + } else { + let mut i = end_str.parse::().map_err(|_| "invalid range")?; + if r.start > i { + return Err("invalid range"); + } + if i >= size { + i = size - 1; + } + r.length = i - r.start + 1; + } + } + ranges.push(r); + } + Ok(ranges) +} + +fn sum_ranges_size(ranges: &[HttpRange]) -> i64 { + ranges.iter().map(|r| r.length).sum() +} + +fn range_content_range(r: HttpRange, total: i64) -> String { + format!("bytes {}-{}/{}", r.start, r.start + r.length - 1, total) +} + +fn range_error_response(mut headers: HeaderMap, msg: &str) -> Response { + if !headers.contains_key(header::CONTENT_TYPE) { + headers.insert( + header::CONTENT_TYPE, + "text/plain; charset=utf-8".parse().unwrap(), + ); + } + let mut response = Response::new(Body::from(msg.to_string())); + *response.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE; + *response.headers_mut() = headers; + response +} + fn handle_range_request( range_str: &str, data: &[u8], mut headers: HeaderMap, state: Option>, ) -> Response { - let total = data.len(); - - // Parse "bytes=start-end" - let range_spec = match range_str.strip_prefix("bytes=") { - Some(s) => s, - None => return (StatusCode::OK, headers, data.to_vec()).into_response(), + let total = data.len() as i64; + let ranges = match parse_range_header(range_str, total) { + Ok(r) => r, + Err(msg) => return range_error_response(headers, msg), }; - // Parse individual ranges - let ranges: Vec<(usize, usize)> = range_spec - .split(',') - .filter_map(|part| { - let part = part.trim(); - if let Some(pos) = part.find('-') { - let start_str = &part[..pos]; - let end_str = &part[pos + 1..]; - - if start_str.is_empty() { - // Suffix range: -N means last N bytes - let mut suffix: usize = end_str.parse().ok()?; - // Go clamps suffix to file size - if suffix > total { - suffix = total; - } - Some((total - suffix, total - 1)) - } else { - let start: usize = start_str.parse().ok()?; - let end = if end_str.is_empty() { - total - 1 - } else { - end_str.parse().ok()? - }; - Some((start, end)) - } - } else { - None - } - }) - .collect(); - if ranges.is_empty() { - return (StatusCode::OK, headers, data.to_vec()).into_response(); - } - - // Clamp range ends and validate (Go clamps end to size-1 instead of returning 416) - let ranges: Vec<(usize, usize)> = ranges - .into_iter() - .map(|(start, mut end)| { - if end >= total { - end = total - 1; - } - (start, end) - }) - .collect(); - for &(start, end) in &ranges { - if start >= total || start > end { - headers.insert( - "Content-Range", - format!("bytes */{}", total).parse().unwrap(), - ); - return (StatusCode::RANGE_NOT_SATISFIABLE, headers).into_response(); - } + return (StatusCode::OK, headers).into_response(); } // If combined range bytes exceed content size, ignore the range (return 200 empty) - let combined_bytes: usize = ranges.iter().map(|&(s, e)| e - s + 1).sum(); - if combined_bytes > total { + if sum_ranges_size(&ranges) > total { return (StatusCode::OK, headers).into_response(); } if ranges.len() == 1 { - let (start, end) = ranges[0]; - let slice = &data[start..=end]; + let r = ranges[0]; headers.insert( "Content-Range", - format!("bytes {}-{}/{}", start, end, total) - .parse() - .unwrap(), + range_content_range(r, total).parse().unwrap(), ); headers.insert( header::CONTENT_LENGTH, - slice.len().to_string().parse().unwrap(), + r.length.max(0).to_string().parse().unwrap(), ); + if r.length <= 0 { + return (StatusCode::PARTIAL_CONTENT, headers).into_response(); + } + let start = r.start as usize; + let end = (r.start + r.length) as usize; + let slice = &data[start..end]; finalize_bytes_response(StatusCode::PARTIAL_CONTENT, headers, slice.to_vec(), state) } else { // Multi-range: build multipart/byteranges response @@ -1420,7 +1446,7 @@ fn handle_range_request( .to_string(); let mut body = Vec::new(); - for (i, &(start, end)) in ranges.iter().enumerate() { + for (i, r) in ranges.iter().enumerate() { // First boundary has no leading CRLF per RFC 2046 if i == 0 { body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); @@ -1429,9 +1455,13 @@ fn handle_range_request( } body.extend_from_slice(format!("Content-Type: {}\r\n", content_type).as_bytes()); body.extend_from_slice( - format!("Content-Range: bytes {}-{}/{}\r\n\r\n", start, end, total).as_bytes(), + format!("Content-Range: {}\r\n\r\n", range_content_range(*r, total)).as_bytes(), ); - body.extend_from_slice(&data[start..=end]); + if r.length > 0 { + let start = r.start as usize; + let end = (r.start + r.length) as usize; + body.extend_from_slice(&data[start..end]); + } } body.extend_from_slice(format!("\r\n--{}--\r\n", boundary).as_bytes()); @@ -1441,10 +1471,12 @@ fn handle_range_request( .parse() .unwrap(), ); - headers.insert( - header::CONTENT_LENGTH, - body.len().to_string().parse().unwrap(), - ); + if !headers.contains_key(header::CONTENT_ENCODING) { + headers.insert( + header::CONTENT_LENGTH, + body.len().to_string().parse().unwrap(), + ); + } finalize_bytes_response(StatusCode::PARTIAL_CONTENT, headers, body, state) } } @@ -1455,91 +1487,33 @@ fn handle_range_request_from_source( mut headers: HeaderMap, state: Option>, ) -> Response { - let total = info.data_size as usize; - - let range_spec = match range_str.strip_prefix("bytes=") { - Some(s) => s, - None => { - headers.insert( - header::CONTENT_LENGTH, - info.data_size.to_string().parse().unwrap(), - ); - return (StatusCode::OK, headers).into_response(); - } + let total = info.data_size as i64; + let ranges = match parse_range_header(range_str, total) { + Ok(r) => r, + Err(msg) => return range_error_response(headers, msg), }; - let ranges: Vec<(usize, usize)> = range_spec - .split(',') - .filter_map(|part| { - let part = part.trim(); - if let Some(pos) = part.find('-') { - let start_str = &part[..pos]; - let end_str = &part[pos + 1..]; - - if start_str.is_empty() { - let mut suffix: usize = end_str.parse().ok()?; - if suffix > total { - suffix = total; - } - Some((total - suffix, total - 1)) - } else { - let start: usize = start_str.parse().ok()?; - let end = if end_str.is_empty() { - total - 1 - } else { - end_str.parse().ok()? - }; - Some((start, end)) - } - } else { - None - } - }) - .collect(); - if ranges.is_empty() { - headers.insert( - header::CONTENT_LENGTH, - info.data_size.to_string().parse().unwrap(), - ); return (StatusCode::OK, headers).into_response(); } - let ranges: Vec<(usize, usize)> = ranges - .into_iter() - .map(|(start, mut end)| { - if end >= total { - end = total - 1; - } - (start, end) - }) - .collect(); - - for &(start, end) in &ranges { - if start >= total || start > end { - headers.insert( - "Content-Range", - format!("bytes */{}", total).parse().unwrap(), - ); - return (StatusCode::RANGE_NOT_SATISFIABLE, headers).into_response(); - } - } - - let combined_bytes: usize = ranges.iter().map(|&(s, e)| e - s + 1).sum(); - if combined_bytes > total { + if sum_ranges_size(&ranges) > total { return (StatusCode::OK, headers).into_response(); } - let read_slice = |start: usize, end: usize| -> Result, std::io::Error> { - let mut buf = vec![0u8; end - start + 1]; + let read_slice = |start: i64, length: i64| -> Result, std::io::Error> { + if length <= 0 { + return Ok(Vec::new()); + } + let mut buf = vec![0u8; length as usize]; info.source .read_exact_at(&mut buf, info.data_file_offset + start as u64)?; Ok(buf) }; if ranges.len() == 1 { - let (start, end) = ranges[0]; - let slice = match read_slice(start, end) { + let r = ranges[0]; + let slice = match read_slice(r.start, r.length) { Ok(slice) => slice, Err(err) => { return ( @@ -1551,9 +1525,7 @@ fn handle_range_request_from_source( }; headers.insert( "Content-Range", - format!("bytes {}-{}/{}", start, end, total) - .parse() - .unwrap(), + range_content_range(r, total).parse().unwrap(), ); headers.insert( header::CONTENT_LENGTH, @@ -1570,8 +1542,8 @@ fn handle_range_request_from_source( .to_string(); let mut body = Vec::new(); - for (i, &(start, end)) in ranges.iter().enumerate() { - let slice = match read_slice(start, end) { + for (i, r) in ranges.iter().enumerate() { + let slice = match read_slice(r.start, r.length) { Ok(slice) => slice, Err(err) => { return ( @@ -1588,7 +1560,7 @@ fn handle_range_request_from_source( } body.extend_from_slice(format!("Content-Type: {}\r\n", content_type).as_bytes()); body.extend_from_slice( - format!("Content-Range: bytes {}-{}/{}\r\n\r\n", start, end, total).as_bytes(), + format!("Content-Range: {}\r\n\r\n", range_content_range(*r, total)).as_bytes(), ); body.extend_from_slice(&slice); } @@ -1600,10 +1572,12 @@ fn handle_range_request_from_source( .parse() .unwrap(), ); - headers.insert( - header::CONTENT_LENGTH, - body.len().to_string().parse().unwrap(), - ); + if !headers.contains_key(header::CONTENT_ENCODING) { + headers.insert( + header::CONTENT_LENGTH, + body.len().to_string().parse().unwrap(), + ); + } finalize_bytes_response(StatusCode::PARTIAL_CONTENT, headers, body, state) }