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
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file httpcneg-0.1.0.tar.gz.
File metadata
- Download URL: httpcneg-0.1.0.tar.gz
- Upload date:
- Size: 27.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d6e5a9a19151d30ec0d3198ff4de68d342620fe95707b755afe23dfe8d4431f2
|
|
| MD5 |
aa84955def7352a9eca8fa928ceff530
|
|
| BLAKE2b-256 |
c55549a4e303171c9119be878edf5b93ce05f6ec029a4dd0da3dc6d1af7b897b
|
File details
Details for the file httpcneg-0.1.0-py3-none-any.whl.
File metadata
- Download URL: httpcneg-0.1.0-py3-none-any.whl
- Upload date:
- Size: 12.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cd5e06e71b5831c642f72e63b7223d4ffd48a6cff14d162fbf1189dc87783707
|
|
| MD5 |
c3662d9a18905be0e1094fba5ff1411c
|
|
| BLAKE2b-256 |
151e3c5986e60b0b164a24dbbf22af0af5adb68c97b2985de674a3e2f0683e30
|