# 🧾 指令

📌 下方 TLS 範例使用目前工作目錄的 `./certs/`；請放入自己的憑證、配對金鑰或用戶端 CA 檔案。驗證也會讀取這些檔案。

每個條目都以固定的表頭開始：語法、沒寫這個指令時的預設值，以及它可以出現的位置。接著說明指令做什麼、拒絕什麼，以及與 Caddy 的不同之處。

📌 本頁描述的是 **v0.2.2**。與 0.1.x 系列及 0.2.0 候選版不同的行為，會以 **0.2.0 變更** 標示；[升級](/zh-TW/start/upgrade/#️-020-的變更) 把這些變更集中在同一處。

📖 本頁只涵蓋語言的一部分。沒有列在這裡的指令仍會由 `pingclair validate` 檢查，而伺服器沒有實作的指令會被指名拒絕，而不是接受後忽略。

目前有六個 Caddy 標準指令會被指名拒絕，其中 `map` 是實際設定最常缺的一個：它把值算一次然後重複使用，而這裡沒有機械式的改寫方式。同樣被拒絕的還有 `tracing`（OpenTelemetry span）、`push`（HTTP/2 伺服器推送）、`invoke`（呼叫具名路由），以及兩個存取日誌指令 `log_append`（在紀錄中加一個欄位）與 `log_name`（逐請求選擇日誌記錄器）。每個拒絕都會指名該指令，因此搬遷時遇到的是清楚的句子，而不是靜默失效。

## basic_auth

```text
Syntax:   basic_auth [<matcher>] [bcrypt|argon2id [<realm>]] {
              <username> <hashed_password>
              ...
          }
Default:  no authentication
Context:  site block, handle, route
```

在請求繼續往下之前，要求 HTTP Basic 認證。區塊中的每一行是一個帳號：使用者名稱與密碼雜湊，絕不是密碼本身。雜湊由 `pingclair hash-password` 產生（[命令列](/zh-TW/reference/command-line/#pingclair-hash-password)）。

指令那一行上的演算法，就是區塊中每個雜湊要比對的演算法，預設為 `bcrypt`。其他演算法名稱都會被拒絕，沒有區塊的 `basic_auth` 也一樣。雜湊不是所宣告演算法的有效雜湊時（包括明文密碼），該行會在載入時被拒絕。

帶有匹配器的 `basic_auth` 會保護所有回應它所匹配請求的路由，包括指令順序排在它前面的 `respond` 或 `reverse_proxy`。`redir` 仍會在它之前回應，與 Caddy 相同。

```caddyfile
http://:8080 {
    basic_auth /admin/* {
        alice $2b$04$aKz8E/FgvYZuyOZpoHXKJuenUlormXHm8m7WJff0S8hMu7ehuMY7i
    }
    respond "ok"
}
```

## bind

```text
Syntax:   bind <host>
Default:  every interface ([::]), or the global default_bind
Context:  site block
```

把網站的每個監聽器限制在一個主機位址上。這個主機會取代網站所監聽的每個位址的主機部分，TCP、TLS 與 HTTP/3 皆然，`pingclair adapt` 會顯示結果位址。IPv6 主機可以加或不加方括號，在監聽器中一律加上方括號：`bind ::1` 會監聽 `[::1]:443`。

HTTPS 網站的自動 HTTP 重新導向監聽器也會監聽 `bind` 主機，因此從其他介面無法到達。

當每個網站綁定不同介面時，兩個網站區塊可以共用同一個連接埠。綁定的介面是網站身分的一部分，因此 `http://:8080 { bind 127.0.0.1 }` 與 `http://:8080 { bind [::1] }` 是兩個網站，各自只能從自己的介面到達。

拒絕條件：

- 超過一個位址會被拒絕：`` `bind 127.0.0.1 ::1` names 2 addresses, and this build binds one ``。請只寫一個位址、用 `[::]` 代表所有介面，或每個介面寫一個網站。
- 受 `bind` 限制的網站，若與監聽所有介面的網站共用連接埠，會被拒絕：一個連接埠就是一個 socket，受限的網站會變得可以從 `bind` 排除的介面到達。請讓該連接埠上的每個網站綁定相同位址，或把其中一個移到別的連接埠。

與 Caddy 的差異：Caddy 會綁定列出的每個位址。Pingclair 讓每個監聽器只在一個介面上，並拒絕第二個位址，而不是忽略它。

**0.2.0 變更**：`bind` 也適用於有明確位址或連接埠的網站，例如 `http://example.test:8080`。先前它只適用於沒有自身位址的網站，這類網站會監聽所有介面。

```caddyfile
http://example.test:8080 {
    bind 127.0.0.1
    respond "loopback only"
}
```

## cache

```text
Syntax:   reverse_proxy <upstream> {
              cache {
                  ttl       <duration>
                  max_size  <bytes>
              }
          }
Default:  disabled; max_size 134217728 when enabled
Context:  reverse_proxy block
```

此 Pingclair 選項啟用 H1／H2 代理回應快取。`ttl` 必填，提供後備有效期限；`max_size` 是所有快取路由共用的正整數位元組容量。容量不一致、零或未知選項會被拒絕。重載調整儲存容量，縮小時立即逐出項目，移除快取時清空。

新鮮度包含上游 `Age`、依 `Date` 推算的年齡及回應延遲；`Expires` 相對於 `Date` 計算。所有 `Vary` 行與指定的請求欄位皆參與變體；無效 `Vary` 或 `Vary: *` 不會儲存。請求的 `no-cache` 與 `no-store` 在所有欄位行依指令名稱辨識。SSE 與 `flush_interval -1` 不納入快取。

```caddyfile
http://:8080 {
    reverse_proxy 127.0.0.1:3000 {
        cache {
            ttl 30s
            max_size 134217728
        }
    }
}
```

沒有可用的來源有效期限時，`ttl` 僅供 `200` 使用；未指定期限的 `404` 與 `410` 最長快取十秒或較短的 ttl，未指定期限的伺服器錯誤不儲存。`206`、`428`、`429`、`431` 與 `511` 永不儲存。快取保留來源位元組，輸出時依各用戶端壓縮，且命中與未命中都套用 `header_down`。HTTP/3 不使用此回應快取。

## encode

```text
Syntax:   encode [*] [<format> ...]
          encode [*] {
              gzip            [<level>]
              zstd
              minimum_length  <bytes>
              match {
                  status  <codes...>
                  header  <field> [<value>]
              }
          }
          encode off
Default:  no compression
Context:  site block
```

壓縮回應。格式依偏好順序列出：用戶端以相同權重接受多種格式時，列在最前面的勝出。支援的格式是 `zstd` 與 `gzip`，單獨寫 `encode` 代表 `gzip`。`encode off` 會關閉該網站的壓縮。

- `gzip <level>` 設定 gzip 等級，1 到 9，預設為 5。`zstd` 使用等級 3。
- `minimum_length` 是會被壓縮的最小本文；預設為 512 位元組。
- `match` 只壓縮符合這些狀態碼或標頭的回應。明確寫出的 `match` 區塊會取代預設的內容類型清單。

壓縮對回應的影響：

- 有 `encode` 的網站，其回應都帶有 `Vary: Accept-Encoding`，未壓縮送出的也一樣。壓縮會把 `Accept-Encoding` 加進既有的 `Vary` 欄位，而不是取代它。
- 帶有強 `ETag` 的代理回應被重新編碼時，會改成弱 `ETag`，因為編碼後的位元組與來源不同。
- 請求或回應上的 `Cache-Control: no-transform` 會停用該回應的壓縮。
- `Accept-Encoding: *` 不會選出任何編碼；除非用戶端寫出 `gzip` 或 `zstd`，否則收到的是未壓縮的本文。
- 大於 8 MiB 的靜態檔案會以未壓縮方式送出。要以壓縮形式提供大檔案，請使用預先壓縮檔（`file_server { precompressed }`）。

靜態與代理編碼使用相同的預設內容類型清單，`identity` 也參與品質協商。部分或沒有本文的代理回應不壓縮；轉換後移除過期 digest 欄位與 H3 完整性 trailers。壓縮失敗會中止回應，不會接上明文。

拒絕條件：

- `encode br` 會在載入時被拒絕。代理未實作串流式 Brotli 編碼器，也不會自動改用 gzip：`` `encode br`: Brotli is not implemented for proxied responses; use `encode zstd gzip` ``。
- 未知的格式，或區塊內未知的設定，會被拒絕。
- 帶有區塊的 `encode off` 會被拒絕。
- 路徑匹配器或具名匹配器會被拒絕，因為壓縮是以網站為單位設定，而不是以路由為單位。匹配所有請求的 `*` 匹配器則可以接受。

**0.2.0 變更**：網站只在 `encode` 要求的地方壓縮，與 Caddy 相同。依賴先前 gzip 預設值的網站必須加上 `encode gzip` 或 `encode zstd gzip`。區塊中的設定現在會生效；先前的版本會把每個區塊都編譯成單純的 gzip。

```caddyfile
example.com {
    encode {
        zstd
        gzip 6
        minimum_length 1024
    }
    file_server
}
```

## file_server

```text
Syntax:   file_server [<matcher>] [<root>] [browse]
          file_server [<matcher>] [<root>] {
              root                  <path>
              index                 <filenames...>
              browse {
                  file_limit <n>
              }
              compress              [off|false]
              precompressed         [br|zstd|gzip ...]
              hide                  <paths...>
              status                <code>
              pass_thru
              disable_canonical_uris
              etag_file_extensions  <extensions...>
          }
Default:  disabled
Context:  site block, handle, route
```

從磁碟提供檔案。它會判斷 MIME 類型、回應位元組範圍請求與條件式請求，並送出 `ETag` 與 `Last-Modified`。檔案來自 `root` 設定的網站根目錄，或只給這個指令用的根目錄。

`ETag` 採用 Caddy 的寫法：`"<以 36 進位表示的修改時間>-<以 36 進位表示的大小>"`，取自檔案的奈秒修改時間與大小，因此在兩台伺服器之間搬移網站時，用戶端與 CDN 已保存的驗證器仍然有效。每種表示各自帶有標籤——gzip 預先壓縮檔以 `-sidecar-gzip` 結尾、即時壓縮的內容以 `-gzip-<level>` 結尾——因為它們的位元組不同。若檔案旁有 `.etag` 側檔（見檔案伺服器選項中的 `etag_file_extensions`），則以該值為驗證器。

- `index` 指定目錄要嘗試的檔案；預設為 `index.html`。索引必須是相對檔名。
- `browse` 為沒有索引檔的目錄產生列表。`file_limit` 限制顯示的項目數；預設上限是 10,000。
- `compress off` 讓這個檔案伺服器在原本會壓縮的網站上免於壓縮。
- `precompressed` 在用戶端接受該編碼時，提供像 `app.js.gz` 這樣的預先壓縮檔。沒有參數時，順序是 `br zstd gzip`。只有寫了這個選項才會提供預先壓縮檔。用戶端接受該編碼時，範圍請求會從預先壓縮檔的位元組提供，`Content-Range` 與 `ETag` 描述的是壓縮後的表示。
- `hide` 讓指定的路徑不被提供，也不出現在列表中。不含 `/` 的模式會隱藏任何同名的路徑元件（`.git` 會隱藏 `/a/.git/b`）；含 `/` 的模式是根目錄下的路徑。重複的行會累加。
- `status` 讓每個檔案都以這個狀態碼回應，適合維護頁面。
- `pass_thru` 把不存在的檔案交給下一個處理器，而不是回應 `404`。
- `disable_canonical_uris` 停用為目錄補上結尾斜線的重新導向。

條件式請求依照 RFC 9110：相符的 `If-None-Match` 或仍然有效的 `If-Modified-Since` 得到 `304`，不成立的 `If-Match` 或 `If-Unmodified-Since` 得到 `412`，而 `If-Range` 已不相符的 `Range` 會以 `200` 得到整個檔案。`GET` 與 `HEAD` 以外的方法會得到附帶 `Allow: GET, HEAD` 的 `405`。不論網站是否壓縮，靜態回應都帶有 `Vary: Accept-Encoding`。

拒絕條件：`fs` 會被拒絕，因為只支援本機檔案系統。超出 100–599 的 `status`、未知的子指令、絕對路徑或含 `..` 的索引、列表模板、`reveal_symlinks`、`sort`，以及 `[` 集合沒有關閉的 `hide` 模式，都會被拒絕。

與 Caddy 的差異：位置參數 `<root>` 是 Pingclair 自己加的。在 Caddy 中，`file_server` 後面單獨的路徑是路徑匹配器。如果設定也必須能在 Caddy 中載入，請優先使用 `root`。

**0.2.0 變更**：`ETag` 改由奈秒精度的修改時間組成，並依內容編碼而不同，所以升級後每個靜態 `ETag` 會改變一次，快取會對每個檔案重新驗證一次。

```caddyfile
localhost:8080 {
    file_server ./public
}
```

預先壓縮 sidecar 使用自身的大小與修改時間產生 ETag，優先於即時壓縮快取；gzip 驗證值包含等級。範圍回應使用 identity 編碼並以有界區塊串流傳送。靜態本文快取共用位元組容量與 16,384 項上限，包含空檔案。標準重新導向清理路徑、跳脫反斜線並保留查詢字串，避免變成其他主機的參照。設定的 `ETag` 標頭就是重新驗證時比對的驗證器。

## forward_auth

```text
Syntax:   forward_auth <upstream> {
              uri <path>
              copy_headers <fields...>
              transport http {
                  tls
                  tls_server_name <name>
                  tls_trusted_ca_certs <files...>
                  tls_client_auth <cert> <key>
                  tls_insecure_skip_verify
              }
          }
Default:  no authentication subrequest
Context:  site block, handle, route
```

先以不帶本文的 GET 向驗證服務發出子請求，並附上原始方法與 URI。2xx 會複製指定的身分標頭再繼續，其他回應串流傳給用戶端。複製前先移除目的標頭，即使目的名稱被改名也一樣。`transport http` 只接受列出的 TLS 選項；未知選項、缺少配對金鑰，或同時設定自訂 CA 與略過驗證會被拒絕。只有明確需要時才停用憑證驗證。

```caddyfile
http://:8080 {
    forward_auth https://auth.example.com {
        uri /check
        copy_headers Remote-User
        transport http {
            tls_server_name auth.example.com
        }
    }
    reverse_proxy 127.0.0.1:3000
}
```

## handle

```text
Syntax:   handle [<matcher>] {
              <directives...>
          }
Default:  none
Context:  site block, handle, route, handle_errors
```

把多個指令組成一條路由。同層的 `handle` 區塊彼此互斥：只有第一個匹配的區塊會執行，即使該區塊沒有寫出回應。有路徑的區塊會依路由的方式排序（見[哪條路由回應](/zh-TW/reference/pingclairfile/#-哪一條路由回應)），沒有匹配器的 `handle` 則是後備。同一個區塊內的每個指令都會依指令順序執行。

匹配器記號只能是 `*`、以 `/` 開頭的路徑，或具名匹配器（`@name`）。其他任何記號都會被拒絕：``expected at most one matcher (`*`, a path starting with `/`, or `@name`) before the block, got `*.php` ``。

**0.2.0 變更**：先前無法辨識的記號會被丟棄，所以 `handle *.php { … }` 會回應網站上的每個請求。請寫成 `@php path *.php` 與 `handle @php { … }`。

```caddyfile
example.com {
    @php path *.php
    handle @php {
        respond "PHP" 200
    }
    handle {
        respond "Not PHP" 200
    }
}
```

## handle_errors

```text
Syntax:   handle_errors [<status|Nxx> ...] {
              <directives...>
          }
Default:  the built-in error text, or the site's error_page
Context:  site block
```

當請求以錯誤狀態結束時執行一條路由。參數是三位數的確切狀態碼或 `Nxx` 範圍，彼此以「或」結合；沒有參數時，區塊會接下所有錯誤。區塊內的 `{err.status_code}` 與其他 `{err.*}` 預留位置描述這個錯誤。

錯誤在兩種情況下會進入這個區塊：處理器引發錯誤時（`error`，或檔案不存在的 `file_server`），以及伺服器自己產生錯誤時：無法連上上游的 `reverse_proxy`（`502`、`503`、`504`）、超過 `request_body` 上限的本文（`413`），以及停止傳送的本文（`408`）。在路由之前就被拒絕的請求——目前是超過上限的標頭區塊——不會進入這個區塊：它會以內建拒絕回應，並指出過大的欄位名稱；`error_page <status> …` 仍可提供該回應主體。

- 區塊內的 `root` 設定錯誤路由自己的文件根目錄，可以寫在區塊的任何位置。以匹配器限定的 `root @name …` 會被拒絕。
- 區塊內的 `file_server` 依錯誤路由的設定提供檔案。頁面會以錯誤的狀態碼送出，失敗請求的 `Range` 與驗證欄位會被忽略，所以錯誤絕不會變成 `206` 或 `304`。
- 在錯誤路由內引發的錯誤會直接回應，不會再次執行錯誤路由。
- 區塊內的 `reverse_proxy` 會在載入時被拒絕。這裡的上游交換是在處理鏈之外的生命週期步驟，該處理器會編譯成功卻什麼都不回應；請在網站路由中代理，並在這裡用 `respond` 或 `file_server` 呈現它的錯誤。區塊內的其他內容都是一般路由主體：指令依 Caddy 的順序執行、`@name` 匹配器可用、`rewrite` 使用區塊自己的已編譯樣式。

**0.2.0 變更**：閘道錯誤與本文大小錯誤會進入 `handle_errors`，錯誤路由中的 `file_server` 會提供它的頁面，而不是錯誤文字。因此，接下所有錯誤的 `handle_errors { … }` 也會回應 `502`、`504` 與 `413`；若只想處理原本設想的錯誤，請為它加上狀態碼。它對閘道錯誤的回應不帶 `Proxy-Status` 欄位，而內建的閘道錯誤會帶。

```caddyfile
example.com {
    reverse_proxy 127.0.0.1:3000
    handle_errors 502 503 504 {
        root * /srv/errors
        rewrite * /{err.status_code}.html
        file_server
    }
}
```

## handle_path

```text
Syntax:   handle_path <path-matcher> {
              <directives...>
          }
Default:  none
Context:  site block, handle, route
```

作用與 `handle` 相同，另外會在區塊內的指令執行前移除匹配到的路徑前綴：`handle_path /api/*` 會把 `/api/users` 以 `/users` 轉送。前綴比對與選中該區塊的路由一樣忽略 ASCII 字母大小寫，所以 `handle_path /API/*` 也會從 `/api/users` 移除 `/api`。匹配器記號的規則與 `handle` 相同。

```caddyfile
example.com {
    handle_path /api/* {
        reverse_proxy 127.0.0.1:3000
    }
}
```

## header

```text
Syntax:   header [<matcher>] <field> [<value> [<replacement>]]
          header [<matcher>] {
              <field> <value>                  # set
              +<field> <value>                 # append
              -<field>                         # remove
              ?<field> <value>                 # set only if absent
              <field> <search> <replacement>   # regular-expression replace
              match {
                  status  <codes...>
                  header  <field> [<value>]
              }
              defer
          }
Default:  none
Context:  site block, handle, route
```

修改回應標頭。直接寫欄位名稱會設定該標頭，加上 `+` 前綴會附加一個值，加上 `-` 前綴會移除該欄位。`?` 前綴只在回應還沒有該欄位時才設定值。有三個參數時，第二個是正規表示式，第三個會取代它匹配到的內容。

標頭永遠套用在完成的回應上，所以 `defer` 與 `>` 前綴會被接受，但不會改變任何事。`header` 區塊內的 `match` 區塊，會讓整個區塊依完成回應的狀態碼（`404`、`2xx`）或標頭決定是否套用。

拒絕條件：

- 同時有參數與區塊的指令會被拒絕。
- 沒有值的 `header X-Name` 會被拒絕。Caddy 會設定一個空值，但空的回應標頭幾乎總是打錯的移除操作。
- 含有 CR、LF 或 NUL 的欄位值，或不是有效記號的欄位名稱，會在載入時被拒絕（RFC 9110 §5.5）。
- `handle_response { header { … } }` 內的 `match` 區塊會被拒絕。

⚠️ 沒有 `set` 關鍵字。區塊中的 `set X-Name value` 這一行，會被解讀成對一個名為 `set` 的標頭進行正規表示式取代。

與 Caddy 的差異：單寫欄位名稱會取代回應已有的值，而 Caddy 的 `header X-Name value` 在不加 `defer` 時會在其旁新增一行欄位。nginx 也是如此——`add_header` 只把欄位加進回應，不會動到上游的——且同樣沒有通用的取代指令：在那裡取代上游欄位要寫 `proxy_hide_header` 加 `add_header`。因此上游以 `X-Name: from-upstream` 回應時，兩者都會保留該行，這裡則不會。要同時保留兩行請寫 `+X-Name`；只在回應沒有該欄位時設定，請寫 `?X-Name`。

`Strict-Transport-Security` 只會在加密的回應上送出，並依 RFC 6797 的要求從每個明文回應中移除。開啟 HSTS 的方法是寫 `header Strict-Transport-Security "max-age=…"`。

```caddyfile
example.com {
    header {
        X-Frame-Options "DENY"
        X-Content-Type-Options "nosniff"
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        -X-Powered-By
    }
}
```

## limits

```text
Syntax:   limits {
              header_timeout          <duration>
              body_timeout            <duration>
              idle_timeout            <duration>
              request_timeout         <duration>
              max_headers             <count>
              max_header_bytes        <bytes>
              max_connections         <count>
              upload_bytes_per_sec    <bytes>
              download_bytes_per_sec  <bytes>
              long_connections {
                  idle_timeout     <duration|off>
                  request_timeout  <duration|off>
              }
          }
Default:  header_timeout 60s; a body may pause 60s between reads
Context:  site block
```

設定網站連線與請求的資源上限。`limits` 是 Pingclair 自己的指令，Caddy 沒有對應的指令。

- `header_timeout` 限制整個請求標頭的時間：從連線被接受（或前一個 keepalive 請求結束）起，到標頭最後一個位元組為止。預設為 60 秒。因此，閒置的 HTTP/1 keepalive 連線在 60 秒內沒有請求就會被關閉，而標頭在這段時間後仍不完整的 HTTP/3 請求串流會以 `H3_REQUEST_INCOMPLETE` 重設。
- `body_timeout` 是請求本文兩次讀取之間允許的最長停頓。沒有設定時，停頓上限是 60 秒。停止傳送的用戶端會得到 `408`；經由 `reverse_proxy` 的 HTTP/2 上傳則改為重設串流。WebSocket 與立即刷新的路由只採用明確設定的值。
- `long_connections` 為 WebSocket 與串流路由覆寫 `idle_timeout` 與 `request_timeout`；`off` 會移除期限。

**0.2.0 變更**：兩個 60 秒的預設值都是新的。刻意長時間不傳資料的用戶端，例如 gRPC 用戶端串流，需要明確設定 `body_timeout`，或在其路由上設定 `flush_interval -1`。

```caddyfile
example.com {
    limits {
        header_timeout 30s
        body_timeout 2m
    }
    reverse_proxy 127.0.0.1:3000
}
```

## listen

```text
Syntax:   listen [http://|https://]<address> [proxy_protocol]
Default:  the listeners named by the site address
Context:  site block
```

為網站加上一個監聽器。`listen` 是 Pingclair 自己的指令，讀法與 nginx 的同名指令相同：`listen 127.0.0.1:8080` 與 `listen [::1]:8080` 綁定該位址；`listen :8080`、`listen 8080` 與 `listen *:8080` 綁定所有介面；沒有連接埠的位址使用 HTTP 連接埠，加上 `https://` 時則使用 HTTPS 連接埠。`proxy_protocol` 要求該監聽器收到 PROXY protocol 標頭。`listen` 指定了位址的網站不會繼承 `default_bind`。

拒絕條件：主機名稱（`listen` 只綁定、從不解析名稱）、沒有方括號的 IPv6 位址、不是 0 到 65535 的連接埠、未知的旗標，以及與網站 `bind` 不一致的位址。

**0.2.0 變更**：`listen` 會保留它寫出的位址。先前的版本只保留連接埠，所以 `listen 127.0.0.1:8080` 會監聽所有介面。要繼續監聽所有介面，請寫 `listen :<port>`。

```caddyfile
http://:8080 {
    listen 127.0.0.1:9090
    respond "two listeners"
}
```

## log

```text
Syntax:   log [<name>] [{ <options> }]
Default:  no access log
Context:  site block; global options
```

寫入存取日誌。網站層級的幾種寫法意義各不相同：

- `log` 為網站開啟預設的存取日誌，輸出到標準輸出。
- `log { … }` 設定網站的存取日誌。
- `log <name> { … }` 為網站加上一個具名的日誌記錄器，有自己的輸出。
- `log <name>` 把網站的紀錄送到在全域選項中以 `log <name> { … }` 宣告的同名通道。

在全域選項區塊中，沒有名稱的 `log { … }` 改為設定伺服器自己的程序日誌：`output file <path>`、`output stdout`、`output stderr`、`format json|text` 與 `level`。檔案輸出端不存在時會以 `0600` 權限建立。`RUST_LOG` 仍然優先於設定的 `level`，啟動橫幅則留在標準輸出。

JSON 記錄的形狀是本伺服器自己的，不是 Caddy 的外層信封，因此針對 Caddy 部署寫的管線會讀到不同的鍵。兩者的對照如下：

| Caddy | 這裡 | 說明 |
| --- | --- | --- |
| `ts` | `ts` | 值與形狀相同：請求開始時的 Unix 秒數，含小數 |
| `.request.uri` | `.path` | 同一個值，提升到最上層 |
| `.request.method` | `.method` | 提升到最上層 |
| `.request.host` | `.host` | 提升到最上層 |
| `.request.proto` | `.protocol` | 改名 |
| `.request.client_ip` | `.client_ip` | 提升到最上層；由 trusted-proxy 政策決定 |
| `.request.headers` | `.request_headers` | 提升到最上層；欄位名轉為小寫 |
| `.resp_headers` | `.response_headers` | 改名 |
| `.size` | `.bytes` | 同一個量：回應主體位元組數 |
| `.duration` | `.duration_ms` | 這裡是**毫秒**，單位寫在名稱裡，且低於 1 毫秒的請求保留小數 |
| `.status` | `.status` | 兩邊唯一原本就同名的鍵 |
| — | `.ttfb_ms` | 首字節時間（毫秒）；Caddy 沒有對應欄位 |
| `.bytes_read` | — | 不記錄請求主體位元組數 |
| `.level`、`.logger`、`.msg` | — | Caddy 的日誌信封；這裡記錄本身就是整個物件 |

存取日誌是監聽器的屬性，與 Caddy 相同：任一網站的 `log` 會為該監聽器服務的每個請求開啟紀錄，而所有網站都沒寫 `log` 的監聽器一行都不寫。`Host` 未匹配任何網站、或在路由前就被拒絕的請求，會寫進程序日誌，也就是預設存取記錄器的位置。

區塊選項包括 `output`（`stdout`、`stderr` 或 `file <path>`）、`format`（`json` 或 `console`）、`level`、`hostnames` 選擇器、`include` 與 `exclude` 過濾器、`sampling`，以及檔案輪替（`roll_size`、`roll_keep`、`roll_keep_for`、`mode`、`dir_mode` 與其他 `roll_*` 選項）。每筆 JSON 存取紀錄都帶有 `ts` 欄位：請求開始時距 Unix 紀元的秒數。

拒絕條件：全域通道不能使用 `hostnames`，因為它不屬於任何網站。宣告兩次的通道會被拒絕。

紀錄在寫入前會先批次累積。跟不上的輸出端會丟棄紀錄，並在 `pingclair_access_log_dropped_total` 中計數。

```caddyfile
example.com {
    log {
        output file /var/log/pingclair/access.log
    }
}
```

## metrics

```text
Syntax:   metrics [<matcher>] [{ disable_openmetrics }]
Default:  no metrics route
Context:  site block, handle, route
```

從網站路由提供 Prometheus 抓取端點，讓抓取程式不必存取 Admin API 就能讀取數據。這條路由和它所在的網站一樣開放；在公開網站上，請在它前面加上匹配器或 `basic_auth`。

只有在收集開啟時，這條路由才會提供數據，而收集由全域 `metrics` 選項控制（見[全域選項](#global-options)）。收集關閉時，它會以空的本文回應 `200`。

輸出格式是 Prometheus 文字（`text/plain; version=0.0.4; charset=utf-8`），且不隨用戶端的 `Accept` 標頭改變：這個建置不協商 OpenMetrics，因此 `disable_openmetrics` 會被接受，並描述已經生效的行為，而不是關掉某個功能。要求 OpenMetrics 的抓取程式會收到 Prometheus 文字，這類抓取程式都能讀取。

```caddyfile
{
    metrics
}

http://:9180 {
    bind 127.0.0.1
    metrics /metrics
}
```

## php_fastcgi

```text
Syntax:   php_fastcgi [<matcher>] <upstream...> {
              root                  <path>
              split                 <suffix...>
              index                 <filename|off>
              try_files             <candidates...>
              env                   <name> <value>
              resolve_root_symlink
              dial_timeout          <duration>
              read_timeout          <duration>
              write_timeout         <duration>
              capture_stderr
          }
Default:  disabled; split .php; index index.php
Context:  site block, handle, route
```

將檔案匹配與重寫展開為 FastCGI 代理，上游可為 PHP-FPM。也接受已支援的反向代理選項。請求本文緩衝政策會到達 FastCGI 傳輸層；腳本路徑保留非 UTF-8 檔名字元的原始位元組，多行 Cookie 會合併。HEAD 不傳本文，下載速率限制會生效；參數超過 FastCGI record 容量回應 `431`，截斷或格式錯誤的本文會中止回應。

沒有宣告本文長度的請求——chunked 上傳，或沒有本文的 `POST`——會在這裡讀取並量測長度，上限為路由的 `request_buffers`；路由未設定時，則以本伺服器的緩衝上限為準。超過上限的無長度本文會得到 `413`。

```caddyfile
http://:8080 {
    root * /srv/php
    php_fastcgi 127.0.0.1:9000 {
        read_timeout 30s
        write_timeout 30s
    }
    file_server
}
```

## request_body

```text
Syntax:   request_body [<matcher>] {
              max_size       <size>
              read_timeout   <duration>
              write_timeout  <duration>
              set            <body>
          }
Default:  no size limit
Context:  site block, handle, route
```

限制或取代請求本文。

- `max_size` 以 `413` 拒絕更大的本文，不論長度是事先宣告的，還是串流中途超過上限。大小依 SI/IEC 區分：`10MB` 是 10,000,000 位元組，`10MiB` 是 10,485,760 位元組。
- `read_timeout` 與 `write_timeout` 限制這條路由讀取本文與寫出回應的時間。停滯的上傳會得到 `408`。
- `set` 以展開預留位置後的文字取代本文。用戶端自己傳來的位元組會在抵達時丟棄，而不是暫存起來。

網站層級、沒有匹配器的 `request_body` 適用於網站中的每個請求，包括在 `handle` 區塊內回應的請求。`handle` 內的 `request_body` 會為該路由覆寫它。

**0.2.0 變更**：除非有設定，否則請求本文沒有大小上限。先前的版本預設會拒絕超過 1 MiB 的代理本文。

```caddyfile
example.com {
    request_body {
        max_size 10MB
    }
    reverse_proxy 127.0.0.1:3000
}
```

## reverse_proxy

```text
Syntax:   reverse_proxy [<matcher>] <upstream> [<upstream> ...]
          reverse_proxy [<matcher>] [<upstream> ...] { ... }
Default:  none
Context:  site block, handle, route
```

把請求轉送到一個或多個上游。`lb_policy` 決定請求如何分配給它們；預設為 `random`，也就是 Caddy 的預設值。`lb_policy first` 會把每個請求都送到第一個可用的上游。

以主機名稱指定的上游，會依全域 `dns_refresh` 設定的間隔重新解析，所以換了位址重新啟動的後端不需要重新載入就能跟上。解析失敗時，先前的位址會繼續留在輪替中。

`header_up` 與 `header_down` 分別在前往上游的請求、以及回程的回應上編輯標頭。兩者與 `header` 指令一樣有四種寫法，意義相同：`X-Name value` 設定、`+X-Name value` 附加、`-X-Name` 移除、`?X-Name value` 僅在欄位不存在時設定，`>X-Name find replacement` 以正規表示式改寫既有值。值可以是模板，因此 `header_up X-Real-IP {client_ip}` 會轉送經 trusted-proxy 政策解析後的位址。

上游自己的 `Server` 欄位行會原樣到達用戶端：Caddy 在處理鏈之前先設定自己的 `Server`，接著代理把上游標頭複製上去而覆蓋它，本伺服器遵循同一規則，只對自己產生的回應加上 `Server: Pingclair`。`Via` 是附加而非取代，並在回應原本經過的鏈之後標示這個中間節點（`1.1 Pingclair`）。回應也會帶著伺服器為每個請求產生的 `X-Request-Id`。

主動健康檢查會在請求之外探測每個上游。失敗的上游會在使用者請求到達之前離開輪替，並在達到設定的成功探測次數後重新加入。`backup` 上游只有在所有主要上游都無法使用時才會被使用。權重為 `0` 的上游會被排空：它不會收到任何請求。

逾時設定寫在 `transport http` 區塊中：`connect_timeout`（Caddy 的 `dial_timeout`）、`first_byte_timeout`（Caddy 的 `response_header_timeout`）、`read_timeout` 與 `write_timeout`。`lb_try_duration` 限制請求抵達後多久之內還能開始新的嘗試；它不會切斷已在進行中的回應。

上游的 `103 Early Hints` 會在最終回應之前送達 HTTP/1.1 用戶端；HTTP/2 路徑會由代理程式庫（`pingora-core 0.9.0`）丟棄過渡回應，HTTP/3 路徑則依設計略過。

Pingclair 自己產生的 `502` 或 `504` 會帶有 `Proxy-Status: pingclair; error=…`，因此可以與後端送出的回應區分。一旦上游可能已經看到請求，自動重試只會重複冪等的方法。

拒絕條件：

- 未知的選項會以完整名稱被拒絕，例如 `Unknown directive 'reverse_proxy: dial_timeout'`。
- 大於 100 的權重會被拒絕，所有主要上游權重都是 0 的上游池也會被拒絕。
- 在這個建置中沒有對應實作的 `transport http` 選項（`read_buffer`、`write_buffer`、`max_conns_per_host`、`keepalive_interval`，以及其他 Caddy 從 Go HTTP 用戶端沿用的選項）會被指名拒絕。

**0.2.0 變更**：

- 預設的 `lb_policy` 是 `random`；要保留先前的輪流分配，請寫 `lb_policy round_robin`。
- `lb_try_duration` 不再切斷慢速回應或長時間的事件串流。請改用 `first_byte_timeout` 或 `read_timeout` 限制慢速後端。
- 在 `trusted_proxies` 後方，`{remote_host}` 是連線的對端，`{client_ip}` 才是用戶端。`header_up X-Real-IP {client_ip}` 會轉送用戶端位址。

```caddyfile
:80 :8080 {
    reverse_proxy {
        lb_policy least_conn
        to 10.0.0.1:8080 {
            weight 3
        }
        to 10.0.0.2:8080
        to 10.0.0.3:8080 {
            backup
        }
        health_check {
            path /health
            interval 5s
            timeout 2s
            status 200 204
            consecutive_failure 3
            consecutive_success 2
        }
    }
}
```

[反向代理指南](/zh-TW/guides/reverse-proxy/)會逐一說明每個選項。

`request_buffers <size|unlimited>` 與 `response_buffers <size|unlimited>` 在轉送前先緩衝，達上限後串流傳送其餘資料；`unlimited` 在此仍有 8 MiB 的記憶體上限。使用 SI／IEC 單位，`1MB` 與 `1MiB` 不同。FastCGI 會套用請求緩衝政策；未宣告 `Content-Length` 的 FastCGI 請求（分塊上傳，或 HTTP/2、HTTP/3 的內容）必須先讀取才能量出長度，因此對這種請求而言上限是硬限制：超過上限會回應 `413`，不會以 PHP-FPM 讀成空內容的形式轉送。`flush_interval -1` 立即排出回應，不納入回應快取。

## root

```text
Syntax:   root [<matcher>] <path>
Default:  none
Context:  site block, handle_errors
```

設定網站根目錄：`file_server`、`try_files` 與其他處理檔案的指令解析路徑時所依據的目錄。`file_server` 可以有自己的根目錄，但在這裡設定，可以讓所有指令都指向同一個位置。在 `handle_errors` 內，`root` 設定錯誤路由自己的根目錄。

```caddyfile
example.com {
    root * /srv/public
    file_server
}
```

拒絕條件：一般 `handle` 或 `route` 內的 `root` 不受支援；帶路徑或具名匹配器的 root 也會被拒絕。網站或錯誤路由請使用 `root * <path>`。

## route

```text
Syntax:   route [<matcher>] {
              <directives...>
          }
Default:  none
Context:  site block, handle, route
```

依撰寫順序執行區塊內的指令，而不是依指令順序。當較窄的指令必須在指令順序排在前面的較寬指令之前執行時，就使用它。匹配器記號的規則與 `handle` 相同。

```caddyfile
example.com {
    route {
        file_server /assets/*
        respond "fallback" 200
    }
}
```

## tls

```text
Syntax:   tls internal
          tls <cert_file> <key_file>
          tls <email>
          tls { <options> }
Default:  automatic HTTPS for public names
Context:  site block
```

控制網站憑證的來源。沒有 `tls` 這一行時，公開名稱會自動從 Let's Encrypt 取得憑證。

| 形式 | 行為 |
| --- | --- |
| `tls internal` | 由持久的本機憑證授權單位簽發：一個根憑證，以及簽發 90 天葉憑證的中繼憑證。用戶端必須信任它的根憑證；`pingclair trust` 會安裝它。 |
| `tls <cert> <key>`，或區塊中的 `cert` 與 `key` | 使用在別處簽發的憑證與金鑰檔。`validate` 會讀取兩個檔案，並拒絕不相符的一對。 |
| `tls <email>` | 設定 ACME 帳號的電子郵件，並保留自動簽發。 |
| `tls { auto }` | 透過 ACME 取得公開憑證並續期，這也是公開名稱的預設行為。 |

區塊也接受 `acme_email`（或 `email`）、`http3`、`default_sni`、`client_auth`、`renewal_window_ratio`，以及 DNS-01 選項（`dns`、`resolvers`、`dns_ttl`、`propagation_delay`、`propagation_timeout`、`dns_challenge_override_domain`）。

網站的每個位址都有憑證：`tls internal` 為每個名稱簽發一張葉憑證，`tls <cert> <key>` 這一對則為網站的每個名稱回應。`*.example.com` 網站會申請一張萬用字元憑證，它恰好涵蓋一層標籤。

`http3 off` 讓這個網站退出 HTTP/3：它的 QUIC 交握會被拒絕，它的回應也不會在 `Alt-Svc` 中宣告 HTTP/3，而同一連接埠上的其他網站保持不變。它不會建立或移除 QUIC 監聽器；由全域 `servers { protocols … }` 清單決定（[TLS：可以調整什麼](/zh-TW/guides/tls-tuning/#-提供哪些協定)）。

`client_auth` 和憑證一樣，依用戶端送出的名稱選擇最具體的網站：沒有 `client_auth` 的確切網站不會要求用戶端憑證，即使同一連接埠上的萬用字元網站會要求。用途擴充欄位排除用戶端驗證的用戶端憑證會被拒絕。

`client_auth` 也接受 `verifier leaf file <paths...>`、`verifier leaf folder <directory>` 或含葉憑證載入器的區塊，在鏈驗證後固定允許的葉憑證。資料夾遞迴掃描 `.pem`，重載時重新掃描；其他 verifier 模組被拒絕。

拒絕條件：

- 裸的 `tls`——沒有參數也沒有區塊——會被拒絕，Caddy 也拒絕它：它什麼都沒有指名，而帶主機名稱的站台位址本來就會自動取得 HTTPS。請寫 `tls internal`、`tls <cert> <key>`、`tls <email>` 或 `tls { … }` 區塊。
- `dns` 只接受 `cloudflare`。其他供應商都會被拒絕：``DNS provider `route53` is not implemented; this build ships `cloudflare` only``。
- `protocols`、`ciphers`、`curves`、`alpn`、`on_demand`、`key_type`、`issuer`，以及上面沒有列出的其他 Caddy 選項，都會被指名拒絕。
- `tls internal` 不能與 `auto`、ACME 電子郵件或憑證檔一起使用。
- 沒有名稱的網站或 `_` 網站上的憑證檔會被拒絕。

**0.2.0 變更**：內部授權單位的檔案移到 `<store>/pki/authorities/local/`，與 Caddy 的配置相同。舊的 `<store>/internal/` 目錄不會被搬移：伺服器會建立新的授權單位，用戶端必須重新信任它的根憑證。

```caddyfile
example.com {
    tls {
        cert ./certs/example.com.pem
        key ./certs/example.com.key
    }
    reverse_proxy localhost:3000
}
```

## try_files

```text
Syntax:   try_files <candidates...> {
              policy first_exist|first_exist_fallback|smallest_size|largest_size|most_recently_modified
          }
Default:  no rewrite; policy first_exist
Context:  site block, handle, route
```

選出檔案候選並重寫請求。每個位置參數都是候選，包括第一個路徑；此指令不接受匹配器 token。候選依設定的根目錄解析，支援佔位符與 glob，保留非 UTF-8 檔名的原始位元組。`=404` 是錯誤後備。未知政策與不安全路徑會被拒絕。

```caddyfile
http://:8080 {
    root * /srv/site
    try_files {path} /index.html
    file_server
}
```

## uri

```text
Syntax:   uri [<matcher>] strip_prefix <prefix>
          uri [<matcher>] strip_suffix <suffix>
          uri [<matcher>] path_regexp <pattern> <replacement>
Default:  unchanged URI
Context:  site block, handle, route
```

修改請求路徑。移除前綴與後綴忽略 ASCII 大小寫，與路徑路由及 `handle_path` 相同。運算前先展開運算元佔位符；正規表示式取代以 `$1` 引用擷取群組。`${1}` 會被視為佔位符 `{1}`，無法保留擷取值。未知操作、錯誤參數數量、區塊與無效正規表示式會被拒絕。

兩個 Caddy 運算會被指名拒絕，而不是勉強近似：`replace` 取代路徑中的子字串，而這裡的 `rewrite` 取代整條路徑；`query` 編輯查詢字串，這裡目前沒有任何東西會改寫它。訊息會指名運算與原因，因此拒絕不會被讀成打錯字。

```caddyfile
http://:8080 {
    uri path_regexp ^/old/(.*)$ /new/$1
    reverse_proxy 127.0.0.1:3000
}
```

## Global options

全域選項寫在檔案最上方的未命名區塊中。Caddy 巢狀放在 `servers { … }` 下的選項，在那裡也可以使用。

| 選項 | 語法 | 說明 |
| --- | --- | --- |
| `acme_dns` | `acme_dns cloudflare <token>` | 全域 Cloudflare DNS-01 憑證；其他 provider 被拒絕。 |
| `client_ip_headers` | `client_ip_headers <field> ...` | 依序查詢的用戶端身分來源，也可放在 `servers`；只有受信任對端可提供。 |
| `default_sni` | `default_sni <name>` | 沒有 SNI 時選用的憑證名稱，包含 HTTP/3；明確但未知的名稱仍被拒絕。 |
| `ocsp_stapling` | `ocsp_stapling off` | 本建置不附 OCSP 回應，因此接受 off；裸指令與 on 被拒絕。 |
| `renewal_window_ratio` | `renewal_window_ratio <ratio>` | 憑證生命週期中的續期比例，可由網站的 `tls` 覆寫。 |
| `admin` | `admin [<address> [<token>]] [{ origins …; enforce_origin }] \| off` | 啟用 Admin API；預設位址為 `127.0.0.1:2019`。有權杖時，請求必須送出 `Authorization: Bearer <token>`；沒有權杖時，只接受迴路用戶端。沒有這個選項就沒有 Admin API（[Admin API](/zh-TW/reference/admin-api/)）。 |
| `auto_https` | `auto_https on \| off \| disable_redirects \| ignore_loaded_certs` | 控制自動 HTTPS 與 80 連接埠的重新導向。`disable_redirects` 不會綁定自動的 HTTP 連接埠。`disable_certs` 會被指名拒絕。 |
| `default_bind` | `default_bind <host>` | 為沒有寫 `bind`、且 `listen` 沒有指定位址的每個網站提供 `bind` 主機。只能寫一個位址。 |
| `dns_refresh` | `dns_refresh <duration> \| off` | 重新解析主機名稱上游的間隔。預設為 `30s`。`off` 會保留啟動時解析到的位址。單獨的數字會被拒絕。 |
| `email` | `email <address>` | ACME 帳號的電子郵件。 |
| `grace_period` | `grace_period <duration>` | 平順停止時讓進行中的請求完成的時間。預設為 `30s`。最後一個請求結束後，程序就會立即結束。 |
| `http_port`、`https_port` | `http_port <port>` | 只寫配置方式的位址（`http://example.com`、`https://example.com`）與自動 HTTPS 所使用的連接埠。預設為 80 與 443。 |
| `log` | `log [<name>] { … }` | 未命名的區塊設定程序日誌；具名區塊宣告一個存取日誌通道（[`log`](#log)）。 |
| `metrics` | `metrics [{ per_host; observe_catchall_hosts }]` | 開啟指標收集。沒有它就不收集任何指標，抓取端點會回應空的內容。`per_host` 為設定中提供服務的主機加上 `host` 標籤。 |
| `order` | `order <directive> first\|last\|before <d>\|after <d>` | 調整某個指令在指令順序中的位置。 |
| `servers` | `servers [<address>] { … }` | 監聽器選項：`protocols`、`trusted_proxies static …`、`client_ip_headers`、`expected_underscore_headers`、`listener_wrappers { proxy_protocol }` 與 `metrics`。指定位址的區塊只套用到那一個監聽器，且只能設定這些選項。 |
| `storage` | `storage file_system <path>` | TLS 儲存區的目錄。優先於 `PINGCLAIR_TLS_STORE`。其他儲存模組會被拒絕。 |
| `trusted_proxies` | `trusted_proxies <cidr> ...` | 可以在轉送標頭中陳述用戶端位址的對端。在 `servers { … }` 內請使用 Caddy 的寫法 `trusted_proxies static <cidr \| private_ranges> ...`。每個範圍只能寫一行。 |

在 `servers` 內，`client_ip_headers <field> ...` 依序列出可以指出用戶端的標頭。沒有它時，用戶端取自 `X-Forwarded-For` 與 `Forwarded`，兩者都沒送時才取 `X-Real-IP`。

有幾個 Caddy 的全域選項會被指名拒絕，而不是接受後忽略：`acme_ca` 與 `acme_ca_root`（自訂 ACME 目錄與簽署其回應的 CA）、`on_demand_tls`（由用戶端握手驅動的簽發）、`filesystem`（具名檔案系統；本建置只註冊本機那一種），以及 `preferred_chains`（偏好的簽發者鏈——這裡的 ACME 用戶端採用憑證頒發機構先回傳的那一條）。每個拒絕都會指名該選項；`preferred_chains` 是唯一在 `validate` 而非 `adapt` 階段被拒絕的，因為文件本身可以轉換，只有佈建步驟無法照辦。`servers { timeouts { … } }` 同樣被拒絕：這裡表達同一組限制的寫法是網站層級的 [`limits`](#limits) 區塊。

**0.2.0 變更**：

- 只有 `client_ip_headers` 列出 `CF-Connecting-IP` 時，它才會指出用戶端。
- 只有設定 `metrics` 才會收集指標。
- `servers` 內的 `trusted_proxies` 必須寫出 `static` 模組名稱，同一範圍內的第二行 `trusted_proxies` 會被拒絕。
- 有多個位址的 `bind` 與 `default_bind` 會被拒絕。

在 `servers` 內，`expected_underscore_headers <name> ...` 允許名稱含底線的請求欄位，結尾的 `*` 代表前綴比對。沒有這個選項時，任何含底線的欄位都不會到達處理常式，這也是 Caddy 的預設行為。指定位址的 `servers <address> { … }` 區塊會以該監聽器的清單取代未指定位址的清單。

```caddyfile
{
    email admin@example.com
    admin 127.0.0.1:2019
    dns_refresh 30s
    servers {
        trusted_proxies static 173.245.48.0/20
        client_ip_headers CF-Connecting-IP
    }
}
```

📚 上述 0.2.0 變動的依據與完整升級清單請見[CHANGELOG](https://github.com/dorianverlaine/pingclair/blob/main/CHANGELOG.md)。

⚠️ 變更全域 `metrics` 後需要重啟服務。透過 `/load` 套用此變更會收到 `409 restart_required`。詳見：[runtime_listeners.rs](https://github.com/dorianverlaine/pingclair/blob/main/pingclair/src/runtime_listeners.rs)。
