🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

標題:別再忽略 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 簡史

RFC 2324 是 IETF 為慶祝愚人節而編寫的 Larry Masinter 的玩笑。它擴展了 HTTP,使其包含以下內容:

  • 新增HTTP 方法BREWWHENPROPFIND

  • 新增一個請求頭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 類型,並要求區分伯爵茶和大吉嶺茶。真是荒謬中的嚴謹。

RFC 實際定義的內容

在編寫任何一行程式碼之前,請先閱讀規範。這就是練習內容。

新方法

MethodRole BREW (或POST )觸發注入GET獲取咖啡壺的當前狀態PROPFIND列出可用的加入項WHEN停止倒牛奶——客戶說“何時!”

WHEN法最為精妙。它將人類的交流(「告訴我什麼時候」)模擬成 HTTP 請求。堪稱協議擬人化的傑作。

Accept-Additions 標頭

BREW /coffee-pot-1 HTCPCP/1.0
Accept-Additions: milk-type=Whole-milk; syrup-type=Vanilla; alcohol-type=Whisky

法定價值包括CreamHalf-and-half牛奶、 Whole-milkNon-Dairy 、糖漿( VanillaChocolateRaspberryAlmond )和烈酒( WhiskyRum 、咖啡Kahlua 、阿誇Aquavit )。

故意省略:任何不含咖啡因的選項。 RFC對此的評論簡潔明了: “這有什麼意義呢?”

第一步-先玩:獨立模擬器

在編寫任何伺服器端程式碼之前,我建立了一個完全獨立的 HTML/JS 模擬器,它完全在瀏覽器中執行。無需後端,無需依賴,無需安裝。

互動式 HTCPCP 控制面板

https://htcpcp.benchwiseunderflow.in/

這個模擬器並非伺服器的簡單模擬——它一個完整的 HTCPCP 實現,只是執行在不同的執行時環境中。所有狀態都保存在 JavaScript 中:包括 pot 註冊表、brew 歷史記錄、狀態轉換以及 418/406 錯誤邏輯。這是在正式部署到堆疊之前快速體驗協定的最佳方式。

試著用茶壺沖泡。觀察418火焰。選擇無咖啡因,得到406。沖泡過程中點選「何時」停止加牛奶。然後回到這裡,建立生產版本。

步驟 2 — 發布:生產伺服器

關於 uvicorn 的一點說明

最自然的反應是uvicorn main:app --reload 。但請不要這樣做。 uvicorn 會在套接字層級驗證 HTTP 方法名稱,然後再進行任何請求解析。 BREWWHENPROPFIND都不是 IANA 註冊的方法,因此無論 FastAPI 配置如何,uvicorn 都會立即拒絕它們,並Invalid HTTP request received BREW

解決方案:使用原始的 asyncio TCP 伺服器( server.py ),並配備一個最小化的 HTTP/1.1 解析器,該解析器接受任何有效的 RFC 7230 令牌作為方法名稱。例如BREWWHENPROPFIND 。這實際上是更正確的方法——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"]),
}

解析 Accept-Additions 標頭

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}
        )

HTCPCP 端點

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"
    })

中介軟體:強制執行 HTCPCP 標頭

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 開發經驗的開發者。

如何以實體的方式思考——例如,壺的登記冊、 CoffeePotTeapot區別、透過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


原文出處:https://dev.to/pascal_cescato_692b7a8a20/stop-ignoring-rfc-2324-its-the-most-important-protocol-youve-never-implemented-53pe


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝20  
573
🥈
我愛JS
💬2  
7
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付