Parse HTTP Link Headers into Named Relations
Parse RFC 8288 Link headers into named relations while respecting commas inside quoted titles and target URLs.
Code
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class Link:
target: str
rel: str
title: str | None = None
def split_parts(header: str) -> list[str]:
parts: list[str] = []
current: list[str] = []
in_quotes = False
in_angle = False
for char in header:
if char == '"':
in_quotes = not in_quotes
elif char == "<" and not in_quotes:
in_angle = True
elif char == ">" and not in_quotes:
in_angle = False
if char == "," and not in_quotes and not in_angle:
parts.append("".join(current).strip())
current = []
else:
current.append(char)
if "".join(current).strip():
parts.append("".join(current).strip())
return parts
def parse_link_header(header: str) -> dict[str, Link]:
links: dict[str, Link] = {}
for part in split_parts(header):
if not part.startswith("<") or ">" not in part:
continue
target, _, rest = part[1:].partition(">")
attributes: dict[str, str] = {}
for attribute in rest.split(";"):
attribute = attribute.strip()
if not attribute or "=" not in attribute:
continue
name, _, raw_value = attribute.partition("=")
attributes[name.strip().lower()] = raw_value.strip().strip('"')
rel = attributes.get("rel")
if rel:
links[rel] = Link(target=target, rel=rel, title=attributes.get("title"))
return links
def main() -> None:
header = (
'<https://api.example.com/items?page=2&limit=20>; rel="next"; '
'title="Next, page 2", '
'<https://api.example.com/items?page=9&limit=20>; rel="last"'
)
links = parse_link_header(header)
print(links["next"].target)
print(links["next"].title)
assert links["last"].target.endswith("page=9&limit=20")
assert parse_link_header("") == {}
if __name__ == "__main__":
main()
Report a problem with this exampleOpens a pre-filled issue on GitHub.