preferhdr
Parse, validate and build the HTTP Prefer and Preference-Applied headers (RFC 7240),
typed and zero-dependency.
Install
pip install preferhdr
Reading a request
parse takes the field value, or several values when the request carries several
Prefer lines, and answers the questions a server asks about the registered preferences:
from preferhdr import parse
prefs = parse(["respond-async, wait=10", 'return=minimal; foo="some parameter"'])
print(prefs.respond_async, prefs.wait, prefs.return_, prefs.handling)
# True 10 minimal None
print(prefs.get("return"))
# return=minimal; foo="some parameter"
applied = prefs.get("return")
assert applied is not None # get() gives None when the preference is absent
print("Wait" in prefs, applied.params["foo"])
# True some parameter
Names are case-insensitive, so they come back lower-cased; values are kept verbatim, and
an empty value (foo="") is the same as no value. When a preference is repeated, only the
first one counts, as RFC 7240 asks:
from preferhdr import parse
print(list(parse('Return=minimal, return=representation, foo=""')))
# [Preference(token='return', value='minimal', params={}), Preference(token='foo', value=None, params={})]
The default parser is tolerant, as a server must be: it accepts whitespace around = and
empty list elements, reads CR, LF and NUL as spaces, and skips a malformed member (or a
malformed parameter) while keeping the rest. It never raises PreferError:
from preferhdr import parse
print(list(parse("wait = 5, bad member, , handling=lenient; x=@")))
# [Preference(token='wait', value='5', params={}), Preference(token='handling', value='lenient', params={})]
Validating a value
strict=True checks the grammar a sender must follow (RFC 7240 with erratum 4439, RFC
9110 lists) and reports the first problem with its position:
from preferhdr import PreferError, parse
try:
parse(["respond-async", "wait =10"], strict=True)
except PreferError as error:
print(error.message, error.position, error.field)
# whitespace before '=' 4 1
Answering
Preferences.applied keeps the preferences a server honoured, without their parameters,
ready for Preference-Applied. build writes any list of preferences, and vary adds
Prefer to the Vary header, which RFC 7240 requires whenever a preference can change
what a cache may reuse:
from preferhdr import Preference, build, parse, vary
prefs = parse("return=representation; x=1, respond-async")
print(build(prefs.applied("return"), applied=True))
# return=representation
print(build([Preference("return", "minimal"), Preference("note", "a, b")]))
# return=minimal, note="a, b"
print(vary("Accept-Encoding"))
# Accept-Encoding, Prefer
Every name and value is checked when the Preference is made, so the builder can never
split a header:
from preferhdr import Preference, PreferError
try:
Preference("return", "minimal\r\nSet-Cookie: a=1")
except PreferError as error:
print(error)
# value of 'return' holds U+000D, which a header field cannot carry
Command line
preferhdr checks a value (or the Prefer lines of a request on stdin, as copied from a
log) and prints one preference per line, exiting with 1 when the value breaks the grammar:
$ preferhdr 'Respond-Async, wait=10, return=minimal; foo="some parameter"'
respond-async
wait=10
return=minimal; foo="some parameter"
$ preferhdr 'wait = 10'
wait=10
invalid: whitespace before '=' at position 4 of field 0
--applied reads Preference-Applied instead, --json prints JSON.
API
| Name | What it does |
|---|---|
parse(value, *, strict=False, applied=False) |
Field value or values to Preferences. |
Preferences |
Immutable sequence: get, in, respond_async, return_, wait, handling, applied(*names). |
Preference(token, value=None, params={}) |
One validated, hashable preference; str() writes it. |
build(prefs, *, applied=False) |
Preferences to one field value. |
vary(current=None) |
A Vary value that lists Prefer once. |
PreferError |
ValueError with message, position and field. |
Where the RFC leaves room
return=minimal, return=representation(and the same withhandling): sections 4.2 and 4.4 say such a request may be treated as if neither was sent; section 2 says the first instance is the one considered. This library applies the first-wins rule, soreturn_is"minimal".Prefer: Lenientnames a preference calledlenient, nothandling=lenient(erratum 4955).- A repeated parameter keeps its first value, like a repeated preference.
respond_asyncisTrueonly for the bare preference;return_andhandlingonly for the values the RFC defines (values are case-sensitive).waitmust be ASCII digits; a value longer than 100 digits reads as no wait at all (it is an upper bound, and no bound means the same thing).- An unterminated quoted string runs to the end of its field value, so the tolerant
parser drops what it belongs to and whatever follows it in that value: the member, or
only the parameter when the string is a parameter value (
a, b; p="q, ckeepsaandb). - Values may hold obs-text as opaque data, U+00A0 to U+00FF. The C1 controls (U+0080 to
U+009F) are rejected although RFC 9110 allows them: U+0085 is a line break to Python's
str.splitlines, and they carry no text. Anything above U+00FF is rejected too. - Names are matched case-insensitively in ASCII only, as tokens are ASCII.
- Strict mode accepts a repeated parameter (no rule forbids it) and keeps the first.
- On stdin the command line reads the header section only (up to the first empty line) and joins folded lines with a space (RFC 9112 section 5.2).
Why
RFC 7240 is small but fiddly: optional values, quoted strings holding commas and semicolons, case-insensitive names, several header lines, first-wins duplicates, and a grammar fixed by errata. Hand-rolled splitting breaks on quoted values and lets CR LF through into responses.
Python has no parser for it. The standard library's urllib.request.parse_http_list
and email.message.Message.get_params can be combined to split such a value, but they
know nothing of RFC 7240: they keep empty and repeated members, fold the case of some
names and not others, and misread a quoted value holding an escaped ". The PyPI
packages with "prefer" in their name (checked 2026-10-19) are about application
settings, not this header, and Werkzeug 3.1.9, Django 6.1.2 and Starlette 1.9.0 do not
read it. In JavaScript, parse-prefer-header 1.0.0 reads it into camel-case keys, drops
the value of a quoted string holding an escaped " and has no strict mode or builder.
The test suite runs both against this library and asserts every difference.
preferhdr is one small, typed, zero-dependency codec for both directions: a tolerant
reader for servers, a strict checker for tests and clients, a builder that cannot split
a header, and typed answers for the registered preferences.
Ground truth
The strict grammar, the registered preferences and the errata are read from the RFC
text, the RFC Editor errata page and the IANA registry, fetched by
scripts/fetch_upstream.py and checked against the hashes in tests/upstream/MANIFEST.json
(the files are not shipped). The GUIDE
gives every rule the parser follows.
License
MIT. See LICENSE.
Links
Metadata
Release files for preferhdr 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| preferhdr-1.0.0.tar.gz | 56.5 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| preferhdr-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 77.7 kB
Release files / preferhdr-1.0.0.tar.gz
| Download URL | preferhdr-1.0.0.tar.gz |
|---|---|
| Size | 56.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
eb571d8fb4af78485749524945d02925cde11193285b0fa8e1c8863ef601b15e
|
|
BLAKE2b-256 checksum How to use checksums |
9c624538403bf6e93a684959e22747abecab8a41b6c5682129abc207030111a0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.2
|
Release files / preferhdr-1.0.0-py3-none-any.whl
| Download URL | preferhdr-1.0.0-py3-none-any.whl |
|---|---|
| Size | 21.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7060738f26e4048a228015c9d7cd8a66952e966664a360c66afd7a88c46df8ce
|
|
BLAKE2b-256 checksum How to use checksums |
71c3674b268acbe0ba65db79a910d117320b6778c97f9cfd5755ebc4c60cb156
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.2
|
Security for preferhdr 1.0.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.