One terminal session. Copy, paste, watch it run.
pip install blackbull-htcpcp
# ---- Coffee mode: start the server ----
python -c "
from blackbull import BlackBull
from blackbull_htcpcp import HtcpcpExtension
app = BlackBull()
HtcpcpExtension(app=app, pot_type='coffee')
app.run(port=8000)
" &
sleep 1 # wait for the server to bind
# ---- Brew coffee with additions ----
curl -w '\n' -X POST -H 'Accept-Additions: cream; sugar; vanilla' http://localhost:8000/pot
# ---- Inspect the pot ----
curl -w '\n' http://localhost:8000/pot
# ---- Ask when it'll be ready ----
curl -w '\n' http://localhost:8000/pot/when
# ---- Teapot mode ----
kill %1 && wait 2>/dev/null
python -c "
from blackbull import BlackBull
from blackbull_htcpcp import HtcpcpExtension
app = BlackBull()
HtcpcpExtension(app=app, pot_type='teapot')
app.run(port=8000)
" &
sleep 1 # wait for the server to bind
# Ask a teapot to brew coffee → 418
curl -w '\n' -X POST http://localhost:8000/pot
# Ask a teapot to brew tea → 200
curl -w '\n' -X POST -H 'Accept-Additions: milk; honey' http://localhost:8000/pot
kill %1 && wait 2>/dev/null
You'll see five JSON responses, each with Content-Type: message/coffeepot:
| # | What you did | Status | Key detail |
|---|---|---|---|
| 1 | POST with cream; sugar; vanilla
|
200 | Additions echoed back in the response |
| 2 | GET the pot state | 200 | "state":"ready" |
| 3 | GET when it's ready | 200 | "ready":true |
| 4 | Teapot + no additions | 418 |
"I'm a teapot" — RFC 7168 §2.1 |
| 5 | Teapot + milk; honey
|
200 | Tea brewing succeeds — RFC 7168 |
Native method support:
BREW,PROPFIND, andWHENare registered as first-class HTTP methods when the underlying BlackBull framework is ≥ 0.42.1. Before BlackBull 0.42.1,POSTserved as the fallback forBREWandGETcoveredPROPFIND/WHEN— both paths produce identical wire behaviour.
blackbull-htcpcp is the first PyPI package to implement both RFC 2324 (the core HTCPCP spec) and RFC 7168 (its tea-pot extension). 53 tests. A documented threat model. And every feature is wired through the framework's public extension API — zero core modifications.
The real goal: stress-test an extension surface
HTCPCP is a joke, but it is a joke with excellent test coverage of HTTP's extension points. A protocol that exercises all five of these in 350 lines is a useful validation tool:
| Extension point | HTCPCP usage |
|---|---|
| Custom HTTP method |
BREW, PROPFIND, WHEN — non-IANA verbs as first-class routes |
| Custom status code |
418 I'm a teapot with application-level semantics |
| Custom content type |
message/coffeepot on every /pot response |
| Application-defined header |
Accept-Additions: cream; sugar; whisky with its own grammar |
| Error semantics | 418 is not "server error" or "client error" — it means "wrong pot type" |
If you can ship all five purely on app.route(), app.on_error(), and app.extensions, the extension surface is real. If you need to modify the framework core, it is not.
Spoiler: no core modifications were needed.
The extension surface
BlackBull's extension model is deliberately minimal — no plugin registry, no dependency injection, no auto-discovery:
# app.route — accepts any RFC 9110 section 5.6.2 token since v0.42.1
@app.route(path='/pot', methods=['BREW', HTTPMethod.POST])
async def brew_handler(scope, receive, send): ...
# app.on_error — custom handler for any HTTP status
@app.on_error(HTTPStatus.IM_A_TEAPOT)
async def teapot_handler(scope, receive, send): ...
# app.extensions — plain dict, stable key
app.extensions['htcpcp'] = self
That is the entire API surface the extension touches. Four @app.route decorators, one app.on_error(418), and one dict assignment. The remaining 300 lines are the Accept-Additions parser, the pot state machine (idle -> brewing -> ready), and the teapot discrimination logic from RFC 7168 section 2.1.
What teapot discrimination looks like in code
RFC 7168 adds nuance: a teapot does not always return 418. Asked to brew tea (with explicit tea additions like milk, lemon, honey), it returns 200. Asked to brew coffee, or asked with no additions at all, it returns 418:
if self._pot_type == 'teapot':
has_coffee = any(a in COFFEE_ADDITIONS for a in additions)
has_tea = any(a in TEA_ADDITIONS for a in additions)
if not has_tea or has_coffee:
await send(self._teapot_response()) # 418
return
# ...proceed to brew (200)
This is the kind of detail that separates "I made a 418 joke endpoint" from "I implemented the RFC." Prior Python implementations (HyperTextCoffeePot, 78 stars, Flask, archived 2015) handled 418 but not RFC 7168 teapot/coffee-pot discrimination.
Security: the threat model
HTCPCP inherits HTTP's full attack surface. Accept-Additions is a semicolon-delimited header parsed from untrusted input — it needs the same hardening as any other HTTP header parser. The test suite documents 15 security cases (S001–S015); the most illustrative are shown below:
| Case | Category | Threat | Mitigation |
|---|---|---|---|
| S001–S003 | Header injection | CRLF / LF / NULL byte in addition tokens | Rejected at parse -> 400 |
| S004–S005 | DoS | Token > 256 chars, or > 64 tokens | Rejected -> 400 |
| S006–S007 | DoS | Concurrent BREW, body > 1 MiB | 409 / 413 |
| S008 | Control chars | Non-printable chars below 0x20 (except HTAB) | Rejected -> 400 |
| S010 | 418 abuse | Cache poisoning via 418 + Cache-Control
|
No permissive cache headers on 418 |
| S014 | Smuggling | BREW + Content-Length / Transfer-Encoding games | Body capped, not interpreted |
| S015 | Replay | Race: two BREW requests simultaneously | State machine guards brewing state |
Every rejection — 400, 409, 413 — returns Content-Type: message/coffeepot. The error format is consistent regardless of which security boundary was hit. The protocol is absurd; the parser is not.
Prior art
Roughly 105 HTCPCP repos on GitHub across all languages. The top implementations by stars:
| Implementation | Language | Stars | Last updated | RFC 7168 |
|---|---|---|---|---|
| HyperTextCoffeePot | Python (Flask) | 78 | 2015 | No |
| madmaze/HTCPCP | C | 49 | 2011 | No |
| node-htcpcp | Node.js | 36 | 2013 | No |
| htcpcp-delonghi | JS (Tessel 2) | 29 | 2023 | No |
| blackbull-htcpcp | Python (BlackBull) | — | 2026 (active) | Yes |
blackbull-htcpcp is the first Python package on PyPI implementing HTCPCP, one of two known implementations covering both RFCs, the only ASGI-native one, and actively maintained with 53 automated tests.
Why joke RFCs matter
RFC 1149 (IP over Avian Carriers, 1990) was implemented by the Bergen Linux User Group in 2001 — 9 packets over 5 km, 55% packet loss, mostly pigeon-related. The #save418 movement in 2017 kept status 418 in Python, Go, and Node.js. Google serves google.com/teapot.
The cultural through-line is that implementing joke protocols is a form of protocol literacy. HTCPCP is small enough to implement in an afternoon, but it touches custom methods, custom status codes, custom content types, header parsing, and error semantics — the same extension points a real application protocol would need.
How this was tested
Every claim above was tested against the published packages before writing.
Test suite: 53/53 passing (pytest tests/ -v in blackbull-htcpcp).
HTTP method validation: BREW, PROPFIND, WHEN all pass BlackBull's RFC 9110 §5.6.2 tchar check, added in v0.42.1 specifically to unblock this extension (changelog).
Framework surface audit: The extension imports only from blackbull (public top-level): BlackBull, Response, read_body, Headers. No blackbull._*, no blackbull.server.*, no monkey-patches.
End-to-end wire output: All responses shown in the terminal session at the top of this article were verified against the published packages using blackbull.testing.TestClient; the curl commands shown produce identical output.
blackbull-htcpcp is on PyPI and GitHub. BlackBull is a pure-Python ASGI server with HTTP/1.1, HTTP/2, and WebSocket at the protocol level.