use reqwest::header::HeaderMap;
pub fn extract_next_link(headers: &HeaderMap) -> Option<String> {
let link = headers.get("link")?.to_str().ok()?;
for (uri, rel) in parse_link_header(link) {
if rel
.split_whitespace()
.any(|tok| tok.eq_ignore_ascii_case("next"))
{
return Some(uri);
}
}
None
}
fn parse_link_header(header: &str) -> Vec<(String, String)> {
let mut out = Vec::new();
let mut depth = 0i32; let mut start = 0usize;
let bytes = header.as_bytes();
let mut segments: Vec<&str> = Vec::new();
for (i, &b) in bytes.iter().enumerate() {
match b {
b'<' => depth += 1,
b'>' => depth -= 1,
b',' if depth <= 0 => {
segments.push(&header[start..i]);
start = i + 1;
}
_ => {}
}
}
segments.push(&header[start..]);
for seg in segments {
let seg = seg.trim();
let Some(lt) = seg.find('<') else { continue };
let Some(gt) = seg[lt + 1..].find('>') else {
continue;
};
let uri = seg[lt + 1..lt + 1 + gt].trim().to_string();
let params = &seg[lt + 1 + gt + 1..];
let mut rel = String::new();
for param in params.split(';') {
let param = param.trim();
if let Some(v) = param
.strip_prefix("rel=")
.or_else(|| param.strip_prefix("rel ="))
{
rel = v.trim().trim_matches('"').to_string();
break;
}
}
out.push((uri, rel));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use reqwest::header::{HeaderMap, HeaderValue};
#[test]
fn test_extract_next_link() {
let mut headers = HeaderMap::new();
headers.insert(
"link",
HeaderValue::from_static(
r#"<https://api.example.com/items?page=2>; rel="next", <https://api.example.com/items?page=5>; rel="last""#
),
);
assert_eq!(
extract_next_link(&headers),
Some("https://api.example.com/items?page=2".to_string()),
);
}
#[test]
fn test_no_next_link() {
let mut headers = HeaderMap::new();
headers.insert(
"link",
HeaderValue::from_static(r#"<https://api.example.com/items?page=1>; rel="prev""#),
);
assert_eq!(extract_next_link(&headers), None);
}
#[test]
fn test_empty_headers() {
let headers = HeaderMap::new();
assert_eq!(extract_next_link(&headers), None);
}
#[test]
fn unquoted_rel_is_supported() {
let mut headers = HeaderMap::new();
headers.insert(
"link",
HeaderValue::from_static("<https://api.example.com/items?page=2>; rel=next"),
);
assert_eq!(
extract_next_link(&headers),
Some("https://api.example.com/items?page=2".to_string())
);
}
#[test]
fn multi_relation_rel_token_matches_next() {
let mut headers = HeaderMap::new();
headers.insert(
"link",
HeaderValue::from_static(r#"<https://api.example.com/p3>; rel="prev next""#),
);
assert_eq!(
extract_next_link(&headers),
Some("https://api.example.com/p3".to_string())
);
}
#[test]
fn comma_inside_url_does_not_split_link() {
let mut headers = HeaderMap::new();
headers.insert(
"link",
HeaderValue::from_static(
r#"<https://api.example.com/items?ids=1,2,3&page=2>; rel="next""#,
),
);
assert_eq!(
extract_next_link(&headers),
Some("https://api.example.com/items?ids=1,2,3&page=2".to_string())
);
}
#[test]
fn rel_next_is_case_insensitive() {
let mut headers = HeaderMap::new();
headers.insert(
"link",
HeaderValue::from_static(r#"<https://api.example.com/x>; rel="NEXT""#),
);
assert_eq!(
extract_next_link(&headers),
Some("https://api.example.com/x".to_string())
);
}
}