Skip to main content

altsvc

Parse, check and build the HTTP Alt-Svc header (RFC 7838), typed and zero-dependency.

Install

pip install altsvc

Read a field value

from altsvc import parse

for alt in parse('h3=":443"; ma=3600; persist=1, h2="alt.example.com:443"'):
    print(alt.alpn, repr(alt.host), alt.port, alt.max_age, alt.persist)
# b'h3' '' 443 3600 True
# b'h2' 'alt.example.com' 443 86400 False

An empty host means the origin's own host. Without ma an alternative is fresh for 86400 seconds (section 3.1). ma is read as ASCII digits only, and a value too large to represent is read as 2147483648, as RFC 9111 says for every delta-seconds value.

clear comes back as CLEAR, also when it sits next to alternatives, because section 3 says it then invalidates all of them. Several field lines are one list:

from altsvc import CLEAR, parse

print(parse('h3=":443", clear') is CLEAR)
# True
print(parse(['h3=":443"', 'h2=":443"; ma=60'])[1].ma)
# 60

Strict and tolerant reading

The default reading is tolerant: it keeps every alternative whose protocol-id, authority and ma are sound, and ignores a broken parameter of another name. RFC 7838 does not say how to recover from a malformed value; this is a choice of the library, and lint tells you what was skipped. strict=True raises on the first error instead:

from altsvc import AltSvcError, lint, parse

value = 'h3=":443"; ma=x, h2=":0", h3=":8443";'
print(parse(value))
# [Alternative(protocol='h3', authority=':8443', params={})]
for issue in lint(value):
    print(issue.code, issue.severity, issue.offset)
# ma.invalid error 11
# authority.port error 20
# syntax.empty-parameter error 36
try:
    parse(value, strict=True)
except AltSvcError as error:
    print(error.code, error.offset)
# ma.invalid 11

Errors break the RFC 7838 grammar or one of its rules, clear written as part of a list included. A few are library policy on top of the RFC: an empty port and port 0 (nothing can connect to them), and IP literals other than plain IPv6 (IPvFuture, zone IDs). Warnings flag legal but likely mistaken values: a repeated parameter (the last one wins, ma included), persist with a value other than 1 (ignored, section 3.1), an ma above 2^31, and a protocol that is not one of the HTTP entries of the IANA ALPN registry.

Build a field value

from altsvc import CLEAR, Alternative, build, encode_protocol_id, parse

alt = Alternative.of("h3", 8443, "2001:db8::1", ma=3600, persist=True)
print(build([alt, Alternative.of("h2", 443)]))
# h3="[2001:db8::1]:8443"; ma=3600; persist=1, h2=":443"
print(build(CLEAR))
# clear
print(encode_protocol_id("w=x:y#z"))
# w%3Dx%3Ay#z
print(parse(build([alt]), strict=True) == [alt])
# True

build checks every part before writing it and raises AltSvcError rather than write a value that would not read back: control characters and non-ASCII text (no header injection through CR LF), a protocol-id that is not in canonical form, a host that is not an ASCII reg-name or IP literal, a port outside 1 to 65535, an ma that is not digits up to 2^31, persist other than 1, and parameter names that repeat up to case.

The guarantee runs one way: whatever build writes reads back strictly to the same alternatives. Not every value that reads strictly can be built again, because build also refuses legal values that a sender has no reason to write, such as persist=yes, an ma above 2^31, non-ASCII text in a quoted value, or ma and MA side by side.

Freshness

from altsvc import expires_at, is_fresh, parse

# RFC 7838 section 3.1: ma=60 on a response that is already 30 seconds old.
(alt,) = parse('h2=":8000"; ma=60')
print(expires_at(alt, received_at=1000.0, age=30))
# 1030.0
print(is_fresh(alt, received_at=1000.0, now=1031.0, age=30))
# False

Times are whatever clock you pass in; the library never reads the clock itself.

Command line

$ curl -sI https://example.org | altsvc
$ altsvc 'h3=":443"; ma=86400'

Prints the alternatives and the findings as JSON. Exit status 0 means the value follows RFC 7838 and a client may use it, 1 that it has errors or comes in a 421 response, 2 a usage error such as input without the field. With curl -sIL the last response is checked. In a 421 response the report says "ignored": true and lists no alternatives, because section 6 says a client ignores that field.

API

Name What it does
parse(value, *, strict=False) Read a value or a list of field lines into alternatives or CLEAR
lint(value) Every finding as an Issue (code, message, severity, offset)
build(alternatives) Write a checked value from alternatives or CLEAR
Alternative protocol, authority, params; alpn, host, port, ma, max_age, persist; Alternative.of(...)
CLEAR, Clear The clear token and its type
encode_protocol_id(name) ALPN name to canonical protocol-id
decode_protocol_id(protocol_id) Protocol-id to ALPN name octets
expires_at(alt, received_at, *, age=0) When the alternative stops being fresh
is_fresh(alt, received_at, now, *, age=0) Whether it is still fresh at now
Issue One finding
AltSvcError A ValueError with code, offset and message
DEFAULT_MAX_AGE, DELTA_SECONDS_MAX 86400 and 2147483648

Where shipping parsers differ

  • quiche (Chromium) refuses the whole field on any error; this library keeps the sound alternatives unless you ask for strict=True.
  • quiche skips whitespace around = in parameters; the grammar has none there, so ma = 60 is an error here and the tolerant reading drops that alternative.
  • curl matches clear without regard to case; RFC 7838 makes it case-sensitive, and so does this library.
  • RFC 7838 does not say whether parameter names are case-sensitive. curl and quiche compare them without case, and so do ma, persist and the duplicate check here.
  • quiche refuses a quoted ma; section 3 says quoted-string forms are processed as RFC 7230 asks, so ma="60" is read as 60 here.

RFC 9460 publishes much the same information in DNS. It has no counterpart to ma (the DNS TTL plays that part) nor to persist, and a client that uses both should keep its connection attempts consistent with each. Resolving those records is out of scope here.

Why

Alt-Svc tells a client that a service is also reachable over another protocol or authority, most often to advertise HTTP/3. In Python the header is usually hand-rolled with a split on commas, which breaks on quoted strings and silently accepts what clients reject. This library is a small, typed, zero-dependency codec for that one header, checked against the RFC text and two shipping parsers.

Development

scripts/check.ps1 runs the gate. The tests that compare the code with the RFCs, the IANA registries and the quiche and curl sources read files that are not committed: python scripts/fetch_upstream.py fetches them into tests/upstream/ and checks each against tests/upstream/MANIFEST.json. Without them those tests skip.

License

MIT. See LICENSE.

Metadata

Release files for altsvc 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 altsvc 0.2.0
File Size Uploaded Provenance
altsvc-0.2.0.tar.gz 35.3 kB Without provenance Details

Built distribution (wheel)

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

Total release size: 57.1 kB

Release files / altsvc-0.2.0.tar.gz

Download URL altsvc-0.2.0.tar.gz
Size 35.3 kB
Tags Source
SHA-256 checksum
How to use checksums
8fb7b64471ca2d48a2f8436682ba6323b6858553946c3726751e18082d77e194
BLAKE2b-256 checksum
How to use checksums
bae47fd043fc33a5c7b8c348c6eab3ce75968d6468b8bae1066bc7c6df9f5016
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

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

Download URL altsvc-0.2.0-py3-none-any.whl
Size 21.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0d03cbd6f251058fb7d14ab12ff92b1c791ceb965e4757c43c0416a245c2ed58
BLAKE2b-256 checksum
How to use checksums
9d1a6cb7239d1601b9c7163f68c907ed5b1804d8c5c47f508708ae2a00bc28bb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Security for altsvc 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