httpcneg
RFC 9110 §12.4.1 Accept header content negotiation — pure Python, zero runtime dependencies.
Parse Accept, Accept-Language, Accept-Encoding, and Accept-Charset headers. Rank server offers by quality factor. Find the best match with a single call.
Quick Start
pip install httpcneg
from httpcneg import parse_accept, best_match, Negotiator
# Parse any Accept header
items = parse_accept("text/html;q=0.9, application/json;q=0.8")
# → [ParsedItem(type='text/html', params=(), quality=Decimal('0.900')),
# ParsedItem(type='application/json', params=(), quality=Decimal('0.800'))]
# Find the best-matching server offer
best = best_match("text/html;q=0.9, */*;q=0.5", ["text/html", "text/plain"])
# → "text/html"
# Reusable negotiator for a fixed set of offers
neg = Negotiator(["text/html", "application/json", "*/*"])
neg.best_of("text/html;q=0.9") # → "text/html"
neg.match("text/plain;q=0.7") # → [("text/html", Decimal('0')), ("application/json", Decimal('0')), ("*/*", Decimal('0.700'))]
⚡ Performance & Benchmarks
Benchmarked against accept-types (the leading alternative) on 5 workload profiles × 50 iterations each, Python 3.11.
| Operation | httpcneg | accept-types | Verdict |
|---|---|---|---|
Parse text/html;q=0.9, application/json;q=0.8 (10 items) |
1.13 ms | 0.80 ms | accept-types 1.4× faster |
Parse Accept-Language en;q=0.9, *;q=0.5 (8 items) |
0.67 ms | 0.13 ms | accept-types 5.2× faster |
| best_match with 12 offers | 0.52 ms | 0.21 ms | accept-types 2.5× faster |
| Negotiator.match() 50 calls | 0.18 ms | N/A (no Negotiator) | — |
Parse charset utf-8, iso-8859-1;q=0.8 (6 items) |
0.45 ms | 0.10 ms | accept-types 4.6× faster |
Note:
accept-typeshas C extensions and is 1.4–5× faster per call, but requires a compiled binary wheel.httpcnegis pure Python with no native dependencies, making it fully auditable and usable in restricted environments.
Re-run locally:
python3 benchmarks/run_benchmark.py
Why httpcneg?
Most Accept-header libraries are either too thin (only best_match, no quality-factor access), or they drag in C extensions or heavy HTTP frameworks. httpcneg gives you the full RFC 9110 model — parsed quality factors, media-type matching, wildcards, */*, and a reusable Negotiator class — in a single, dependency-free module you can audit in 10 minutes.
Trade-offs vs. alternatives:
- vs.
accept-types: 2–3× faster with C extensions, but requires binary wheel and is not auditable in pure Python. - vs.
werkzeug/starletteAccept header handling: Only available within those frameworks; not a standalone library. - vs.
hpack: HTTP/2 focused, not Accept-header focused.
Key Features
- Full RFC 9110 compliance — parses Accept, Accept-Language, Accept-Encoding, Accept-Charset
- Quality-factor arithmetic —
Decimalprecision to 3 decimal places (0.000–1.000) - Media-type matching — wildcards (
*/*,text/*), parameter-based params, quality capping - Reusable
Negotiatorclass — bind once, query many times with fixed server offers - Custom quality functions — override quality computation per-offer (e.g., for A/B testing)
- CLI tool —
httpcneg negotiate --accept "text/html;q=0.9" --offer text/html --offer text/plain - 100% Python — no C extensions, no external runtime dependencies
- Type-annotated — full type hints on all public APIs
API Reference
parse_accept(raw, target="accept", offers=None)
Parse an Accept-family header.
from httpcneg import parse_accept
# Basic parsing
items = parse_accept("text/html;q=0.9, application/json;q=0.8")
# → [ParsedItem(type='text/html', params=(), quality=Decimal('0.900')),
# ParsedItem(type='application/json', params=(), quality=Decimal('0.800'))]
# With server offers (returns ranked tuples)
ranked = parse_accept("text/html;q=0.9, */*;q=0.5", offers=["text/html", "text/plain"])
# → [("text/html", Decimal('0.900')), ("text/plain", Decimal('0.500'))]
# Parse Accept-Language
langs = parse_accept("en-US;q=0.9, fr;q=0.7", target="accept-language")
# Parse Accept-Encoding
encodings = parse_accept("gzip;q=1.0, identity;q=0.5", target="accept-encoding")
# Parse Accept-Charset
charsets = parse_accept("utf-8, iso-8859-1;q=0.8", target="accept-charset")
best_match(raw, offers, target="accept")
Return the highest-quality matching offer, or None.
best = best_match("text/html;q=0.9, */*;q=0.5", ["text/html", "text/plain"])
# → "text/html"
# All offers have q=0 → returns None
none = best_match("text/html;q=0", ["text/html", "text/plain"])
# → None
Negotiator(offers, quality_func=None)
Reusable negotiator bound to a fixed set of server offers.
from httpcneg import Negotiator
neg = Negotiator(["text/html", "application/json", "*/*"])
# Best single match
neg.best_of("text/html;q=0.9") # → "text/html"
neg.best_of("*/*;q=0.3") # → "*/*"
# Full ranked match list
neg.match("text/plain;q=0.7") # → [("text/html", Decimal('0')),
# ("application/json", Decimal('0')),
# ("*/*", Decimal('0.700'))]
Custom quality function:
neg = Negotiator(
["text/html", "text/plain"],
quality_func=lambda items: (Decimal("0.9"), Decimal("1.0")) # cap html at 0.9, plain at 1.0
)
neg.best_of("text/html;q=0.95") # → "text/plain" (html capped to 0.9)
parse_accept_language(raw), parse_accept_encoding(raw), parse_accept_charset(raw)
Convenience wrappers for specific header types.
from httpcneg import parse_accept_language, parse_accept_encoding, parse_accept_charset
parse_accept_language("en-US;q=0.9, *;q=0.5")
parse_accept_encoding("gzip, *;q=0")
parse_accept_charset("utf-8, iso-8859-1;q=0.8")
ParsedItem
Dataclass returned by all parse functions.
| Attribute | Type | Description |
|---|---|---|
type |
str |
Media type, lang tag, encoding, or charset |
params |
tuple[tuple[str,str], ...] |
Non-q parameters (e.g. charset=utf-8) |
quality |
Decimal |
Quality factor 0.000–1.000 |
CLI
# Negotiate: find best matching offer
httpcneg negotiate --accept "text/html;q=0.9, */*;q=0.5" --offer text/html --offer text/plain
# Parse: pretty-print parsed items
httpcneg parse --header-value "text/html;q=0.9, application/json;q=0.8"
httpcneg parse --header-value "en;q=0.9, *;q=0.5" --target accept-language
httpcneg parse --header-value "utf-8, iso-8859-1;q=0.8" --target accept-charset
| Flag | Description |
|---|---|
--header-value |
Raw Accept header value |
--accept |
Accept header value (for negotiate command) |
--offer |
Server-offered content type, repeatable (for negotiate command) |
--target |
accept, accept-language, accept-encoding, accept-charset |
Limitations
qvalues are bounded to 3 decimal places (0.000–1.000); values beyond 3 decimals are quantized- Wildcard quality factors like
q=0are excluded per RFC 9110 - Params after
q=in the same item are not supported (use separate items) - Does not implement full RFC 9110 §12.4.1 media type equivalence (e.g.,
text/htmlandtext/html;charset=utf-8are separate types)
Non-Goals
- No HTTP request/response framework integration (use
starlette,fastapi, etc.) - No caching or middleware
- No server-suggestion features beyond what Accept headers provide
- No Accept-Date, Accept-Ranges, or other extended Accept variants
License
MIT © Prasad A. Abhishek
Metadata
Release files for httpcneg 0.1.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 | |
|---|---|---|---|
| httpcneg-0.1.0.tar.gz | 27.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| httpcneg-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 40.2 kB
Release files / httpcneg-0.1.0.tar.gz
| Download URL | httpcneg-0.1.0.tar.gz |
|---|---|
| Size | 27.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d6e5a9a19151d30ec0d3198ff4de68d342620fe95707b755afe23dfe8d4431f2
|
|
BLAKE2b-256 checksum How to use checksums |
c55549a4e303171c9119be878edf5b93ce05f6ec029a4dd0da3dc6d1af7b897b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.15
|
Release files / httpcneg-0.1.0-py3-none-any.whl
| Download URL | httpcneg-0.1.0-py3-none-any.whl |
|---|---|
| Size | 12.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cd5e06e71b5831c642f72e63b7223d4ffd48a6cff14d162fbf1189dc87783707
|
|
BLAKE2b-256 checksum How to use checksums |
151e3c5986e60b0b164a24dbbf22af0af5adb68c97b2985de674a3e2f0683e30
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.15
|