標題:別再忽略 RFC 2324 了。這是你從未實現過的最重要的協議。
已發布:是
描述:HTCPCP/1.0、418 茶壺、一個完整的互動式儀表板,以及為什麼 1998 年愚人節 RFC 比大多數技術書籍更能教導我們軟體設計。
標籤:趣味、HTTP、Python、Web開發
封面圖:"https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xu62xxyp4bk85oc1nzdl.jpg"
有些 RFC 改變了世界-TCP/IP、HTTP/2、TLS 1.3。
此外,還有 1998 年 4 月 1 日發布的RFC 2324 ,它定義了超文本咖啡壺控制協議(HTCPCP/1.0)。其目的是:透過網路控制、監控和診斷咖啡壺。
不,這不是玩笑。好吧,它確實是個玩笑。但它寫得足夠嚴肅,完全可以付諸實踐。 Emacs 做到了,我們也會做到。
RFC 2324 是 IETF 為慶祝愚人節而編寫的 Larry Masinter 的玩笑。它擴展了 HTTP,使其包含以下內容:
新增HTTP 方法: BREW 、 WHEN 、 PROPFIND
新增一個請求頭: Accept-Additions (用於牛奶、糖、威士忌——沒錯,就是威士忌)
新的URI 方案: coffee:// (以及koffie:// 、 café://和其他 26 種翻譯)
兩個改變網路歷史的新錯誤程式碼
406 Not Acceptable — The server cannot brew this coffee
418 I'm a teapot — The server is a teapot, not a coffee pot
418成為了一個標誌性的命名空間。 2017 年,一項將其從 IANA 命名空間移除的提案引發了開發者社群的強烈抗議。 Node.js、Go、Python——所有人都保留了它。最終,418 贏得了勝利。
2014 年, RFC 7168將此協議擴展到茶( HTCPCP-TEA ),增加了message/teapot MIME 類型,並要求區分伯爵茶和大吉嶺茶。真是荒謬中的嚴謹。
在編寫任何一行程式碼之前,請先閱讀規範。這就是練習內容。
MethodRole BREW (或POST )觸發注入GET獲取咖啡壺的當前狀態PROPFIND列出可用的加入項WHEN停止倒牛奶——客戶說“何時!”
WHEN法最為精妙。它將人類的交流(「告訴我什麼時候」)模擬成 HTTP 請求。堪稱協議擬人化的傑作。
BREW /coffee-pot-1 HTCPCP/1.0
Accept-Additions: milk-type=Whole-milk; syrup-type=Vanilla; alcohol-type=Whisky
法定價值包括Cream 、 Half-and-half牛奶、 Whole-milk 、 Non-Dairy 、糖漿( Vanilla 、 Chocolate 、 Raspberry 、 Almond )和烈酒( Whisky 、 Rum 、咖啡Kahlua 、阿誇Aquavit )。
故意省略:任何不含咖啡因的選項。 RFC對此的評論簡潔明了: “這有什麼意義呢?”
在編寫任何伺服器端程式碼之前,我建立了一個完全獨立的 HTML/JS 模擬器,它完全在瀏覽器中執行。無需後端,無需依賴,無需安裝。
互動式 HTCPCP 控制面板
https://htcpcp.benchwiseunderflow.in/
這個模擬器並非伺服器的簡單模擬——它是一個完整的 HTCPCP 實現,只是執行在不同的執行時環境中。所有狀態都保存在 JavaScript 中:包括 pot 註冊表、brew 歷史記錄、狀態轉換以及 418/406 錯誤邏輯。這是在正式部署到堆疊之前快速體驗協定的最佳方式。
試著用茶壺沖泡。觀察418火焰。選擇無咖啡因,得到406。沖泡過程中點選「何時」停止加牛奶。然後回到這裡,建立生產版本。
最自然的反應是uvicorn main:app --reload 。但請不要這樣做。 uvicorn 會在套接字層級驗證 HTTP 方法名稱,然後再進行任何請求解析。 BREW、 WHEN和PROPFIND都不是 IANA 註冊的方法,因此無論 FastAPI 配置如何,uvicorn 都會立即拒絕它們,並Invalid HTTP request received BREW
解決方案:使用原始的 asyncio TCP 伺服器( server.py ),並配備一個最小化的 HTTP/1.1 解析器,該解析器接受任何有效的 RFC 7230 令牌作為方法名稱。例如BREW 、 WHEN和PROPFIND 。這實際上是更正確的方法——HTCPCP 定義了自己的協議,而自行實現傳輸層才是誠實的實現方式。
python server.py
# ☕ HTCPCP/1.0 — RFC 2324 (127.0.0.1:2324)
curl -X BREW http://localhost:2324/coffee/pot-1 \
-H "Accept-Additions: milk-type=Whole-milk; alcohol-type=Whisky"
FastAPI + main.py仍然有一個用途:測試套件。 FastAPI 的TestClient完全繞過了 HTTP 傳輸層,因此自訂方法在測試中可以正常運作——而且你還能獲得 FastAPI 的所有驗證和模式優勢。
pytest test_htcpcp.py -v # uses main.py + TestClient, no server.py needed
第一個架構決策:正確地對實體進行建模。
from enum import Enum
from dataclasses import dataclass, field
class PotType(str, Enum):
COFFEE = "coffee"
TEAPOT = "teapot"
class PotStatus(str, Enum):
IDLE = "idle"
BREWING = "brewing"
POURING_MILK = "pouring-milk"
READY = "ready"
@dataclass
class CoffeePot:
id: str
pot_type: PotType
capacity: int
level: int
status: PotStatus = PotStatus.IDLE
varieties: list[str] = field(default_factory=list)
brew_history: list[dict] = field(default_factory=list)
# The registry — the core of the architecture
POT_REGISTRY: dict[str, CoffeePot] = {
"coffee://pot-1": CoffeePot("pot-1", PotType.COFFEE, 12, 8,
varieties=["Espresso", "Lungo", "Americano"]),
"coffee://pot-2": CoffeePot("pot-2", PotType.COFFEE, 6, 2,
varieties=["Espresso"]),
"tea://kettle-1": CoffeePot("kettle-1", PotType.TEAPOT, 8, 6,
varieties=["Earl Grey", "Chamomile", "Darjeeling"]),
}
from fastapi import Request, HTTPException
SUPPORTED_ADDITIONS = {
"milk-type": ["Cream", "Half-and-half", "Whole-milk", "Part-Skim", "Skim", "Non-Dairy"],
"syrup-type": ["Vanilla", "Almond", "Raspberry", "Chocolate"],
"sweetener-type": ["Sugar", "Honey"],
"spice-type": ["Cinnamon", "Cardamom"],
"alcohol-type": ["Whisky", "Rum", "Kahlua", "Aquavit"],
}
def parse_accept_additions(header: str | None) -> dict[str, str]:
if not header:
return {}
additions = {}
for part in header.split(";"):
part = part.strip()
if "=" in part:
key, value = part.split("=", 1)
additions[key.strip()] = value.strip()
return additions
def validate_additions(additions: dict) -> None:
# RFC 2324 §2.1.1: no decaf option — intentionally
if "decaf" in additions:
raise HTTPException(
status_code=406,
detail={
"error": "Not Acceptable",
"message": "Decaffeinated coffee? What's the point?",
"rfc": "RFC 2324 §2.1.1"
}
)
unsupported = [
f"{k}={v}" for k, v in additions.items()
if k in SUPPORTED_ADDITIONS and v not in SUPPORTED_ADDITIONS[k]
]
if unsupported:
raise HTTPException(
status_code=406,
detail={"error": "Not Acceptable", "unsupported_additions": unsupported}
)
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI(title="HTCPCP/1.0", version="1.0")
def get_pot(pot_id: str) -> CoffeePot:
uri = f"coffee://{pot_id}"
pot = POT_REGISTRY.get(uri) or POT_REGISTRY.get(f"tea://{pot_id}")
if not pot:
raise HTTPException(status_code=404, detail="Pot not found in registry")
return pot
# ── BREW ────────────────────────────────────────────────────────────────────
@app.api_route("/coffee/{pot_id}", methods=["BREW", "POST"])
async def brew(pot_id: str, request: Request):
pot = get_pot(pot_id)
# RFC 2324 §2.3.2: teapot → 418, mandatory
if pot.pot_type == PotType.TEAPOT:
return JSONResponse(status_code=418, content={
"status": 418,
"error": "I'm a teapot",
"body": "The requested entity body is short and stout.",
"hint": "Tip me over and pour me out.",
"pot_id": pot_id,
"rfc": "RFC 2324 §2.3.2",
"suggestion": "Use coffee://pot-1/brew instead"
})
if pot.level == 0:
raise HTTPException(status_code=503, detail="Pot is empty. Refill required.")
additions_header = request.headers.get("accept-additions")
additions = parse_accept_additions(additions_header)
validate_additions(additions) # 406 if decaf or invalid additions
brew_id = len(pot.brew_history) + 1
pot.brew_history.append({"id": brew_id, "additions": additions})
pot.status = PotStatus.BREWING
pot.level -= 1
# Milk requested → enter pouring-milk state
has_milk = "milk-type" in additions
if has_milk:
pot.status = PotStatus.POURING_MILK
return JSONResponse(status_code=200, content={
"brew_id": brew_id,
"message": "Coffee is brewing.",
"pot": pot_id,
"accept-additions": additions,
"milk_pouring": has_milk,
"protocol": "HTCPCP/1.0"
})
# ── GET ──────────────────────────────────────────────────────────────────────
@app.get("/coffee/{pot_id}/status")
def get_status(pot_id: str):
pot = get_pot(pot_id)
return {
"pot_id": pot_id,
"type": pot.pot_type,
"status": pot.status,
"level": f"{pot.level}/{pot.capacity} cups",
"brew_count": len(pot.brew_history),
"varieties": pot.varieties,
"protocol": "HTCPCP/1.0"
}
# ── PROPFIND ─────────────────────────────────────────────────────────────────
@app.api_route("/coffee/{pot_id}/additions", methods=["PROPFIND"])
def propfind(pot_id: str):
get_pot(pot_id)
return {
**SUPPORTED_ADDITIONS,
"decaf": "NOT_ACCEPTABLE — What's the point? (RFC 2324 §2.1.1)"
}
# ── WHEN ─────────────────────────────────────────────────────────────────────
@app.api_route("/coffee/{pot_id}/stop-milk", methods=["WHEN"])
def when(pot_id: str):
"""
RFC 2324 §2.1.3 — WHEN
Sent when the client determines that enough milk has been poured.
The server must stop immediately.
"""
pot = get_pot(pot_id)
if pot.status != PotStatus.POURING_MILK:
return JSONResponse(status_code=200, content={
"message": "WHEN acknowledged.",
"note": "No milk was being poured, but your enthusiasm is appreciated.",
"rfc": "RFC 2324 §2.1.3"
})
pot.status = PotStatus.BREWING
return JSONResponse(status_code=200, content={
"message": "Milk pouring stopped.",
"detail": "The server has acknowledged WHEN and stopped the milk stream.",
"protocol": "HTCPCP/1.0",
"rfc": "RFC 2324 §2.1.3"
})
from starlette.middleware.base import BaseHTTPMiddleware
class HTCPCPMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
response = await call_next(request)
response.headers["X-Protocol"] = "HTCPCP/1.0"
response.headers["X-RFC"] = "RFC-2324"
# Detect a BREW on a non-coffee route and punish accordingly
if request.method == "BREW" and not request.url.path.startswith("/coffee"):
return JSONResponse(status_code=418, content={
"error": "Wrong universe",
"hint": "BREW is only valid on coffee:// URIs"
})
return response
app.add_middleware(HTCPCPMiddleware)
import structlog
log = structlog.get_logger()
# After a successful BREW:
log.info("htcpcp.brew",
pot_id=pot_id,
brew_id=brew_id,
additions=additions,
status_code=200,
protocol="HTCPCP/1.0"
)
# On 418:
log.warning("htcpcp.teapot_detected",
pot_id=pot_id,
pot_type="teapot",
status_code=418,
message="Teapot attempted to brew coffee"
)
這會在 JSON 日誌中產生:
{"event": "htcpcp.brew", "pot_id": "pot-1", "brew_id": 3,
"additions": {"milk-type": "Whole-milk", "alcohol-type": "Whisky"},
"status_code": 200, "protocol": "HTCPCP/1.0", "level": "info"}
{"event": "htcpcp.teapot_detected", "pot_id": "kettle-1",
"status_code": 418, "level": "warning"}
執行愚人節玩笑式的 RFC 文件是一項偽裝得非常嚴肅的練習。最終你會學到:
如何正確閱讀 RFC——區分 MUST、SHOULD 和 MAY。 RFC 2324 謹慎地使用了這三種規則。如果伺服器是一台茶壺,那麼 418 就是必須的。一台壞掉的咖啡機應該要回傳 503,而不是 418。這是一個常見的錯誤,而且很重要。
HTTP 協定堆疊的實際運作原理-嘗試使用BREW執行 uvicorn 時會發現,方法驗證發生在套接字級別,先於 h11,再於 FastAPI,甚至在你編寫程式碼之前。最終,你需要編寫一個原始的 asyncio TCP 伺服器才能真正實作 HTCPCP。這並非繞彎路,而是關鍵。現在,你對 HTTP 請求管道的理解甚至超過了大多數擁有多年生產 API 開發經驗的開發者。
如何以實體的方式思考——例如,壺的登記冊、 CoffeePot和Teapot區別、透過coffee:// URI 進行路由:這才是真正的領域建模。這個玩笑迫使你認真對待它。
如何對狀態機進行建模—— idle → brewing → pouring-milk → ready這是一個教科書式的工作流程。 WHEN 是一個由客戶端驅動的轉換。你會在生產系統中隨處看到這種模式。
如何為看似荒謬但實用的極端情況編寫整合測試:
def test_teapot_cannot_brew():
response = client.request("BREW", "/coffee/kettle-1")
assert response.status_code == 418
assert response.json()["error"] == "I'm a teapot"
def test_decaf_is_not_acceptable():
response = client.request("BREW", "/coffee/pot-1",
headers={"Accept-Additions": "decaf=true"})
assert response.status_code == 406
def test_when_stops_milk():
client.request("BREW", "/coffee/pot-1",
headers={"Accept-Additions": "milk-type=Whole-milk"})
response = client.request("WHEN", "/coffee/pot-1/stop-milk")
assert response.status_code == 200
assert "stopped" in response.json()["message"]
418 號 RFC 之所以能經得起所有扼殺的嘗試,是因為它代表了一種現實:開發者可以盡情發揮創意。今天發布的愚人節 RFC 很可能在一周內就被委員會否決。而 1998 年發布的 418 號 RFC 卻已經存在了 26 年之久。
RFC 2324 的卓越之處在於它認真對待了荒謬之處——它擁有真實的狀態機、語義精確的真實錯誤程式碼,以及一個真實的擴展(RFC 7168 用於茶)。它透過完美地遵循形式主義來嘲諷它。
這正是我們應該建立自己系統的方式。
模擬器(HTML/JS 獨立版)、 server.py (原始 TCP)、 main.py + 完整測試套件 — 在 Github 上: https://github.com/pcescato/htcpcp/ 。
RFC 2324: https://tools.ietf.org/html/rfc2324 RFC 7168:https: //tools.ietf.org/html/rfc7168