Skip to main content

preferhdr

Lianes8 Certified

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 with handling): 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, so return_ is "minimal".
  • Prefer: Lenient names a preference called lenient, not handling=lenient (erratum 4955).
  • A repeated parameter keeps its first value, like a repeated preference.
  • respond_async is True only for the bare preference; return_ and handling only for the values the RFC defines (values are case-sensitive).
  • wait must 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, c keeps a and b).
  • 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.

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)

Source distribution for preferhdr 1.0.0
File Size Uploaded Provenance
preferhdr-1.0.0.tar.gz 56.5 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for preferhdr 1.0.0
File Interpreter ABI Platform
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.

Release history Release notifications | RSS feed

This release

1.0.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