Structured Fields (RFC 9651)¶
Every HTTP header defined by the IETF since ~2021 — Priority,
Deprecation, Accept-Query, rate-limit signalling, Client Hints —
uses Structured Field Values (RFC 9651) instead of a bespoke
grammar. BlackBull ships a strict parser and serialiser for all of
it in blackbull.protocol.structured_fields, verified against the
HTTP Working Group's
conformance suite
(2,135 cases).
Reading structured headers¶
Headers exposes one accessor per top-level SF type. Each returns
None when the field is absent or fails strict parsing — RFC 9651
requires a malformed field to be ignored in its entirety:
@app.route(path='/', methods=[HTTPMethod.GET])
async def handler(conn: Connection):
# Dictionary-typed field, e.g. Priority: u=2, i
priority = conn.headers.get_sf_dict(b'priority')
# → {'u': (2, {}), 'i': (True, {})} or None
# Item-typed field, e.g. Deprecation: @1659578233
deprecation = conn.headers.get_sf_item(b'deprecation')
# → (Date(1659578233), {}) or None
# List-typed field, e.g. Example: sugar, tea;kind=green
example = conn.headers.get_sf_list(b'example')
# → [(Token('sugar'), {}), (Token('tea'), {'kind': Token('green')})]
Multiple field lines of the same name are combined with ", " before
parsing, exactly as §4.2 of the RFC specifies.
The type mapping¶
Every member comes back as a (value, parameters) tuple, where
parameters is an ordered dict[str, bare item]. An Inner List is a
(list_of_items, parameters) tuple. Bare items map to Python as:
| SF type | Python type | Wire example |
|---|---|---|
| Integer | int |
42 |
| Decimal | float |
4.5 |
| String | str |
"hello" |
| Token | Token (str subclass) |
text/html |
| Byte Sequence | bytes |
:cHJl...: |
| Boolean | bool |
?1 |
| Date | Date |
@1659578233 |
| Display String | DisplayString (str subclass) |
%"f%c3%bc" |
Token and DisplayString compare equal to plain str but keep
their identity so re-serialisation stays faithful (foo vs "foo" vs
%"foo" are three different wire forms of the same Python string).
Date wraps an integer of Unix seconds rather than subclassing
datetime — the RFC's range (±10¹⁵−1 seconds) exceeds what datetime
can hold. Use Date.to_datetime() / Date.from_datetime() for
values in the representable range.
Producing structured headers¶
The serialisers return ASCII bytes ready to drop into a response header list:
from blackbull.protocol.structured_fields import (
Date, Token, serialize_dictionary, serialize_item, serialize_list,
)
serialize_list([(Token('gzip'), {}), (Token('br'), {'q': 0.9})])
# b'gzip, br;q=0.9'
serialize_dictionary({'u': (2, {}), 'i': (True, {})})
# b'u=2, i'
serialize_item(Date(1659578233))
# b'@1659578233'
Serialisation validates its input — an integer beyond ±(10¹⁵−1), an
invalid Token, or a non-ASCII String raises ValueError instead of
emitting a malformed field. An empty List or Dictionary serialises to
b''; per RFC 9651 §3.1 you should then omit the field entirely.
Parsing functions (parse_item, parse_list, parse_dictionary) are
also available directly when you have a raw value in hand; they raise
ValueError on any grammar violation.
Where the framework itself uses it¶
The RFC 9218 priority surfaces are parsed with this module: the
Priority request header and the HTTP/2 PRIORITY_UPDATE frame
payload both flow through parse_priority_field, and the result lands
on the scope as described in HTTP/2 → Priority hints.
Out-of-range or mistyped members (u=9, i=1) are ignored per
RFC 9218 §4, and a field value that fails SF parsing falls back to the
defaults (urgency=3, incremental=False).