Skip to content

Commit 849d4f4

Browse files
drcaramelsyrupgumpt
authored andcommitted
Add or remove accept-ranges on range header filter
1 parent a3aa6cb commit 849d4f4

File tree

2 files changed

+117
-36
lines changed

2 files changed

+117
-36
lines changed

pingora-http/src/case_header_name.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ fn titled_header_name(header_name: &HeaderName) -> Bytes {
8585

8686
pub(crate) fn titled_header_name_str(header_name: &HeaderName) -> Option<&'static str> {
8787
Some(match *header_name {
88-
header::AGE => "Age",
8988
header::ACCEPT_RANGES => "Accept-Ranges",
89+
header::AGE => "Age",
9090
header::CACHE_CONTROL => "Cache-Control",
9191
header::CONNECTION => "Connection",
9292
header::CONTENT_TYPE => "Content-Type",

pingora-proxy/src/proxy_cache.rs

Lines changed: 116 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,15 +1310,6 @@ pub mod range_filter {
13101310
return RangeType::None;
13111311
}
13121312

1313-
// "A server MUST ignore a Range header field received with a request method other than GET."
1314-
if req.method != http::Method::GET && req.method != http::Method::HEAD {
1315-
return RangeType::None;
1316-
}
1317-
1318-
let Some(range_header) = req.headers.get(RANGE) else {
1319-
return RangeType::None;
1320-
};
1321-
13221313
// Content-Length is not required by RFC but it is what nginx does and easier to implement
13231314
// with this header present.
13241315
let Some(content_length_bytes) = resp.headers.get(CONTENT_LENGTH) else {
@@ -1329,41 +1320,65 @@ pub mod range_filter {
13291320
return RangeType::None;
13301321
};
13311322

1332-
// if-range wants to understand if the Last-Modified / ETag value matches exactly for use
1333-
// with resumable downloads.
1334-
// https://datatracker.ietf.org/doc/html/rfc9110#name-if-range
1335-
// Note that the RFC wants strong validation, and suggests that
1336-
// "A valid entity-tag can be distinguished from a valid HTTP-date
1337-
// by examining the first three characters for a DQUOTE,"
1338-
// but this current etag matching behavior most closely mirrors nginx.
1339-
if let Some(if_range) = req.headers.get(IF_RANGE) {
1340-
let ir = if_range.as_bytes();
1341-
let matches = if ir.len() >= 2 && ir.last() == Some(&b'"') {
1342-
resp.headers.get(ETAG).is_some_and(|etag| etag == if_range)
1343-
} else if let Some(last_modified) = resp.headers.get(LAST_MODIFIED) {
1344-
last_modified == if_range
1345-
} else {
1346-
false
1347-
};
1348-
if !matches {
1323+
// At this point the response is allowed to be served as ranges
1324+
// TODO: we can also check Accept-Range header from resp. Nginx gives uses the option
1325+
// see proxy_force_ranges
1326+
1327+
fn request_range_type(
1328+
req: &RequestHeader,
1329+
resp: &ResponseHeader,
1330+
content_length: usize,
1331+
max_multipart_ranges: Option<usize>,
1332+
) -> RangeType {
1333+
// "A server MUST ignore a Range header field received with a request method other than GET."
1334+
if req.method != http::Method::GET && req.method != http::Method::HEAD {
13491335
return RangeType::None;
13501336
}
1351-
}
13521337

1353-
// TODO: we can also check Accept-Range header from resp. Nginx gives uses the option
1354-
// see proxy_force_ranges
1338+
let Some(range_header) = req.headers.get(RANGE) else {
1339+
return RangeType::None;
1340+
};
13551341

1356-
let mut range_type = parse_range_header(
1357-
range_header.as_bytes(),
1358-
content_length,
1359-
max_multipart_ranges,
1360-
);
1342+
// if-range wants to understand if the Last-Modified / ETag value matches exactly for use
1343+
// with resumable downloads.
1344+
// https://datatracker.ietf.org/doc/html/rfc9110#name-if-range
1345+
// Note that the RFC wants strong validation, and suggests that
1346+
// "A valid entity-tag can be distinguished from a valid HTTP-date
1347+
// by examining the first three characters for a DQUOTE,"
1348+
// but this current etag matching behavior most closely mirrors nginx.
1349+
if let Some(if_range) = req.headers.get(IF_RANGE) {
1350+
let ir = if_range.as_bytes();
1351+
let matches = if ir.len() >= 2 && ir.last() == Some(&b'"') {
1352+
resp.headers.get(ETAG).is_some_and(|etag| etag == if_range)
1353+
} else if let Some(last_modified) = resp.headers.get(LAST_MODIFIED) {
1354+
last_modified == if_range
1355+
} else {
1356+
false
1357+
};
1358+
if !matches {
1359+
return RangeType::None;
1360+
}
1361+
}
1362+
1363+
parse_range_header(
1364+
range_header.as_bytes(),
1365+
content_length,
1366+
max_multipart_ranges,
1367+
)
1368+
}
1369+
1370+
let mut range_type = request_range_type(req, resp, content_length, max_multipart_ranges);
13611371

13621372
match &mut range_type {
1363-
RangeType::None => { /* nothing to do*/ }
1373+
RangeType::None => {
1374+
// At this point, the response is _eligible_ to be served in ranges
1375+
// in the future, so add Accept-Ranges, mirroring nginx behavior
1376+
resp.insert_header(&ACCEPT_RANGES, "bytes").unwrap();
1377+
}
13641378
RangeType::Single(r) => {
13651379
// 206 response
13661380
resp.set_status(StatusCode::PARTIAL_CONTENT).unwrap();
1381+
resp.remove_header(&ACCEPT_RANGES);
13671382
resp.insert_header(&CONTENT_LENGTH, r.end - r.start)
13681383
.unwrap();
13691384
resp.insert_header(
@@ -1386,6 +1401,7 @@ pub mod range_filter {
13861401
let total_length = multi_range_info.calculate_multipart_length();
13871402

13881403
resp.set_status(StatusCode::PARTIAL_CONTENT).unwrap();
1404+
resp.remove_header(&ACCEPT_RANGES);
13891405
resp.insert_header(CONTENT_LENGTH, total_length).unwrap();
13901406
resp.insert_header(
13911407
CONTENT_TYPE,
@@ -1403,6 +1419,7 @@ pub mod range_filter {
14031419
// empty body for simplicity
14041420
resp.insert_header(&CONTENT_LENGTH, HeaderValue::from_static("0"))
14051421
.unwrap();
1422+
resp.remove_header(&ACCEPT_RANGES);
14061423
// TODO: remove other headers like content-encoding
14071424
resp.remove_header(&CONTENT_TYPE);
14081425
resp.insert_header(&CONTENT_RANGE, format!("bytes */{content_length}"))
@@ -1429,6 +1446,21 @@ pub mod range_filter {
14291446
let mut resp = gen_resp();
14301447
assert_eq!(RangeType::None, range_header_filter(&req, &mut resp, None));
14311448
assert_eq!(resp.status.as_u16(), 200);
1449+
assert_eq!(
1450+
resp.headers.get("accept-ranges").unwrap().as_bytes(),
1451+
b"bytes"
1452+
);
1453+
1454+
// no range, try HEAD
1455+
let mut req = gen_req();
1456+
req.method = Method::HEAD;
1457+
let mut resp = gen_resp();
1458+
assert_eq!(RangeType::None, range_header_filter(&req, &mut resp, None));
1459+
assert_eq!(resp.status.as_u16(), 200);
1460+
assert_eq!(
1461+
resp.headers.get("accept-ranges").unwrap().as_bytes(),
1462+
b"bytes"
1463+
);
14321464

14331465
// regular range
14341466
let mut req = gen_req();
@@ -1444,11 +1476,31 @@ pub mod range_filter {
14441476
resp.headers.get("content-range").unwrap().as_bytes(),
14451477
b"bytes 0-1/10"
14461478
);
1479+
assert!(resp.headers.get("accept-ranges").is_none());
1480+
1481+
// regular range, accept-ranges included
1482+
let mut req = gen_req();
1483+
req.insert_header("Range", "bytes=0-1").unwrap();
1484+
let mut resp = gen_resp();
1485+
resp.insert_header("Accept-Ranges", "bytes").unwrap();
1486+
assert_eq!(
1487+
RangeType::new_single(0, 2),
1488+
range_header_filter(&req, &mut resp, None)
1489+
);
1490+
assert_eq!(resp.status.as_u16(), 206);
1491+
assert_eq!(resp.headers.get("content-length").unwrap().as_bytes(), b"2");
1492+
assert_eq!(
1493+
resp.headers.get("content-range").unwrap().as_bytes(),
1494+
b"bytes 0-1/10"
1495+
);
1496+
// accept-ranges stripped
1497+
assert!(resp.headers.get("accept-ranges").is_none());
14471498

14481499
// bad range
14491500
let mut req = gen_req();
14501501
req.insert_header("Range", "bytes=1-0").unwrap();
14511502
let mut resp = gen_resp();
1503+
resp.insert_header("Accept-Ranges", "bytes").unwrap();
14521504
assert_eq!(
14531505
RangeType::Invalid,
14541506
range_header_filter(&req, &mut resp, None)
@@ -1459,6 +1511,7 @@ pub mod range_filter {
14591511
resp.headers.get("content-range").unwrap().as_bytes(),
14601512
b"bytes */10"
14611513
);
1514+
assert!(resp.headers.get("accept-ranges").is_none());
14621515
}
14631516

14641517
// Multipart Tests
@@ -1507,6 +1560,7 @@ pub mod range_filter {
15071560
format!("multipart/byteranges; boundary={boundary_str}")
15081561
);
15091562
assert!(resp.headers.get("content_length").is_none());
1563+
assert!(resp.headers.get("accept-ranges").is_none());
15101564

15111565
// overlapping range, multipart range is declined
15121566
let req = gen_req_overlap_range();
@@ -1516,6 +1570,10 @@ pub mod range_filter {
15161570
assert!(matches!(result, RangeType::None));
15171571
assert_eq!(resp.status.as_u16(), 200);
15181572
assert!(resp.headers.get("content-type").is_none());
1573+
assert_eq!(
1574+
resp.headers.get("accept-ranges").unwrap().as_bytes(),
1575+
b"bytes"
1576+
);
15191577

15201578
// bad multipart range
15211579
let mut req = gen_req();
@@ -1525,6 +1583,7 @@ pub mod range_filter {
15251583
let result = range_header_filter(&req, &mut resp, None);
15261584
assert!(matches!(result, RangeType::Invalid));
15271585
assert_eq!(resp.status.as_u16(), 416);
1586+
assert!(resp.headers.get("accept-ranges").is_none());
15281587
}
15291588

15301589
#[test]
@@ -1565,6 +1624,11 @@ pub mod range_filter {
15651624
.unwrap();
15661625
let mut resp = gen_resp();
15671626
assert_eq!(RangeType::None, range_header_filter(&req, &mut resp, None));
1627+
assert_eq!(resp.status.as_u16(), 200);
1628+
assert_eq!(
1629+
resp.headers.get("accept-ranges").unwrap().as_bytes(),
1630+
b"bytes"
1631+
);
15681632

15691633
// match ETag
15701634
let mut req = gen_req();
@@ -1574,17 +1638,29 @@ pub mod range_filter {
15741638
RangeType::new_single(0, 2),
15751639
range_header_filter(&req, &mut resp, None)
15761640
);
1641+
assert_eq!(resp.status.as_u16(), 206);
1642+
assert!(resp.headers.get("accept-ranges").is_none());
15771643

15781644
// non-matching ETags do not result in range
15791645
let mut req = gen_req();
15801646
req.insert_header("If-Range", "\"4567\"").unwrap();
15811647
let mut resp = gen_resp();
15821648
assert_eq!(RangeType::None, range_header_filter(&req, &mut resp, None));
1649+
assert_eq!(resp.status.as_u16(), 200);
1650+
assert_eq!(
1651+
resp.headers.get("accept-ranges").unwrap().as_bytes(),
1652+
b"bytes"
1653+
);
15831654

15841655
let mut req = gen_req();
15851656
req.insert_header("If-Range", "1234").unwrap();
15861657
let mut resp = gen_resp();
15871658
assert_eq!(RangeType::None, range_header_filter(&req, &mut resp, None));
1659+
assert_eq!(resp.status.as_u16(), 200);
1660+
assert_eq!(
1661+
resp.headers.get("accept-ranges").unwrap().as_bytes(),
1662+
b"bytes"
1663+
);
15881664

15891665
// multipart range with If-Range
15901666
let mut req = get_multipart_req();
@@ -1593,6 +1669,7 @@ pub mod range_filter {
15931669
let result = range_header_filter(&req, &mut resp, None);
15941670
assert!(matches!(result, RangeType::Multi(_)));
15951671
assert_eq!(resp.status.as_u16(), 206);
1672+
assert!(resp.headers.get("accept-ranges").is_none());
15961673

15971674
// multipart with matching ETag
15981675
let req = get_multipart_req();
@@ -1608,6 +1685,10 @@ pub mod range_filter {
16081685
let mut resp = gen_resp();
16091686
assert_eq!(RangeType::None, range_header_filter(&req, &mut resp, None));
16101687
assert_eq!(resp.status.as_u16(), 200);
1688+
assert_eq!(
1689+
resp.headers.get("accept-ranges").unwrap().as_bytes(),
1690+
b"bytes"
1691+
);
16111692
}
16121693

16131694
pub struct RangeBodyFilter {

0 commit comments

Comments
 (0)