Skip to main content

viaheader

Parse, build and append the HTTP Via header per RFC 9110: strict or safely lenient, typed, zero-dependency.

Install

pip install viaheader

Usage

Read a Via header

from viaheader import parse

hops = parse("1.0 fred, HTTP/1.1 p.example.net:8080 (Varnish/7.4)")
for hop in hops:
    print(hop.protocol, hop.version, hop.host, hop.port, hop.comment)
# HTTP 1.0 fred None None
# HTTP 1.1 p.example.net 8080 Varnish/7.4

# Several field lines are read in order, each as its own list; None means no header.
print(len(parse(["1.0 a", "1.1 b, 1.1 c"])), parse(None))
# 3 []

Each Hop carries received_by (a host or pseudonym with an optional :port, split into host and port; port is None when absent or above 65535), the protocol version, the protocol name (HTTP when the entry gives only a version, and any spelling of HTTP is stored as HTTP) and the decoded comment, with quoted-pairs resolved and nested parentheses kept, or None. Values are str holding the header bytes decoded as latin-1, and parsing takes linear time whatever a client sends.

Lenient by default, strict on request

from viaheader import ViaError, parse

# A client opened a comment it never closed; the entry the proxy appended still reads.
print(parse("1.0 client (, 1.1 edge"))
# [Hop(received_by='edge', version='1.1', protocol='HTTP', comment=None)]

try:
    parse("1.0 client (, 1.1 edge", strict=True)
except ViaError as error:
    print(error, error.position)
# unterminated comment (line 0, offset 11) 11

The lenient parser skips an invalid entry and resumes at the first comma after the point where it broke. For a comment that never closes, that point is its opening parenthesis, so text written before an entry can never swallow or rewrite it. It also replaces CR, LF and NUL with spaces (RFC 9110 section 5.5) and reads the bracketed IPv6 received-by that RFC 7230 allowed. strict=True raises ViaError, a ValueError with the field line and the position, for anything outside the RFC 9110 grammar.

Forward a request

from viaheader import Hop, append, has_loop

received = "1.1 lb.internal:8080 (squid), 1.0 client ("
me = Hop("edge", "1.1", comment="viaheader")

print(has_loop(received, "edge"), has_loop(received + ", 1.1 Edge:443", "edge"))
# False True
print(append(received, me, pseudonyms={"lb.internal": "lb"}))
# 1.1 lb (squid), 1.1 edge (viaheader)

append writes the received hops back clean, in order, and adds yours at the end: an entry that cannot be read is dropped instead of passed on, pseudonyms replaces hosts behind a firewall (RFC 9110 sections 7.6.3 and 17.12) and drop_comments=True removes received comments. pseudonyms keys are hosts without a port, matched without regard to case. Entries received in the RFC 7230 bracketed IPv6 form are kept as they are, so map them in pseudonyms when the next hop expects strict RFC 9110. Received comments are passed on with their commas, which is valid but misleads a downstream parser that splits on every comma; use drop_comments=True when the next hop might be one.

has_loop matches your names without regard to case, and IPv6 literals in any spelling; a name without a port matches any port. A client can write your name into its own Via too, so a match proves a loop only when the entry came from a proxy you trust.

Build a header

from viaheader import Hop, build, parse

value = build([Hop("gw", "1.0", protocol="FTP", comment="a ) b"), Hop("proxy", "1.1")])
print(value)
# FTP/1.0 gw (a \) b), 1.1 proxy
print(parse(value, strict=True)[0].comment)
# a ) b

build writes only valid RFC 9110: it raises ViaError for a protocol, version or received-by that is not a token (with an optional port) and for a comment holding CR, LF, another control or a character above U+00FF. It escapes backslashes and unpaired parentheses in comments, so no header can be injected, no hop can be forged for an RFC 9110 parser, and parse(build(hops)) == hops.

Command line

$ curl -sI https://example.com | python -m viaheader
$ python -m viaheader --strict "1.0 fred, 1.1 p.example.net"

Prints the hops as JSON. Without values it reads a header block from stdin as latin-1 and uses its Via lines; with several responses (curl -sIL) the last one counts, and a body after the blank line is not read. With --strict an invalid value exits with status 1 and the reason.

API

Name What it does
parse(value, *, strict=False) Hops of a value, a list of field lines, or None
build(hops) One Via value, validated and escaped
append(existing, hop, *, pseudonyms=None, drop_comments=False) The value to forward
has_loop(value, name, *names) Whether one of your names is already in the chain
Hop Frozen dataclass: received_by, version, protocol, comment, host, port
ViaError ValueError raised for invalid values, with line and position

Why

Via is how proxies record the chain a message went through and how they stop request loops, but its grammar has corners: an optional protocol name, comments that nest and hold escaped parentheses and commas, a list that may span field lines. A splitter on commas gets those wrong, and a parser that lets a client's unclosed comment run on hides the very entry a loop check is looking for. viaheader reads it the way RFC 9110 defines it, writes it so nothing can be injected, and has no dependencies.

License

MIT. See LICENSE.

Metadata

Release files for viaheader 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for viaheader 0.2.0
File Size Uploaded Provenance
viaheader-0.2.0.tar.gz 27.5 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for viaheader 0.2.0
File Interpreter ABI Platform
viaheader-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 43.9 kB

Release files / viaheader-0.2.0.tar.gz

Download URL viaheader-0.2.0.tar.gz
Size 27.5 kB
Tags Source
SHA-256 checksum
How to use checksums
0594e3723bd3ac0fac7a85ff6754836b036c633c30b79344ccecd5e0983c356d
BLAKE2b-256 checksum
How to use checksums
0119ac69bc7d755a4ae713c5c038f6566bba560b2738c24d0abc36ea72f33e74
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Release files / viaheader-0.2.0-py3-none-any.whl

Download URL viaheader-0.2.0-py3-none-any.whl
Size 16.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
78e3ed8732faa968f9be9e096a89a248fa330c5313cb6504ba11cc8fc7337f13
BLAKE2b-256 checksum
How to use checksums
977d6b05a2f0dfa705c48afeb854114ed9fc1312a5b06fb5b2ea653a3f9ae4bf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Security for viaheader 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page