跳到內容
getnextpdf.com

Enterprise 版本

Webhook — 深入參考

NextPDF\Enterprise\Webhook 命名空間為工作事件提供租戶範圍的 webhook 傳遞。公開範圍為六個符號:WebhookManagerWebhookRegistrationWebhookPayloadWebhookDeliveryWebhookRetryPolicyDeadLetterEntry。manager 逐租戶登錄端點,並將工作事件派送給訂閱中的登錄。傳遞引擎會 POST 一份經 HMAC-SHA256 簽署的 JSON 酬載,針對 Core SSRF 出向閘門驗證每一個目的地,以指數退避重試,並將永久性失敗記錄於記憶體中的死信佇列。自 3.1.0 起,簽章會將 X-NextPDF-Timestamp 標頭綁入 MAC 基礎字串,使接收方一併驗證新鮮度與完整性。工作流程層級的指南請見 Webhook

此能力隨附於 NextPDF Enterprisenextpdf/enterprise),並以 Enterprise 層級的授權封套啟用。未持有該權益的部署不會載入此能力的類別。比較版本並取得授權

webhook 範圍是一項基礎 Enterprise 能力,只要安裝 Enterprise 套件即可使用;沒有獨立的逐功能旗標。NextPDF Core(Apache-2.0)與 NextPDF Pro 沒有 webhook 登錄或傳遞範圍;manager、登錄、酬載、傳遞引擎、重試原則與死信項目僅隨附於 nextpdf/enterprise

符號參數預設行為回傳拋出或失敗於註記
WebhookManager::__constructWebhookDelivery $delivery?LoggerInterface $logger = null建立一個帶有空的記憶體登錄索引的 manager新的 WebhookManager不會拋出登錄是逐租戶索引
WebhookManager::registerTenantContext $tenantWebhookRegistration $registration將登錄附加到呼叫租戶的索引void當登錄的租戶與情境租戶不符時拋出 InvalidArgumentException跨租戶登錄在儲存前即被拒絕
WebhookManager::unregisterTenantContext $tenantstring $registrationId以停用的副本取代相符的登錄bool不會拋出;找不到 id 時回傳 false軟停用;歷史予以保留
WebhookManager::activeRegistrationsTenantContext $tenant將該租戶的登錄篩選為有效者list<WebhookRegistration>不會拋出只有呼叫租戶的登錄可見
WebhookManager::dispatchTenantContext $tenantJobEvent $event將事件傳遞給每一個訂閱該事件型別的有效登錄int(成功傳遞數)當事件資料無法 JSON 編碼時傳播 JsonException;傳遞失敗不會拋出每次登錄傳遞會產生一個全新的 32-hex 傳遞 id
WebhookRegistration::__constructstring $idstring $tenantIdstring $urlarray $eventsstring $secretbool $active = true?string $description = null原封不動地儲存所提供的值新的 WebhookRegistration未宣告 @throws;在 strict_types 下 PHP 會對不符的引數型別拋出 TypeErrorfinal readonly;空的 $events 代表訂閱全部
WebhookRegistration::subscribesToJobEventType $eventType$events 為空或包含該型別時為 truebool不會拋出嚴格同一性比較
WebhookRegistration::deactivate回傳一份停用的副本self不會拋出原始實例維持不變
WebhookPayload::fromJobEventJobEvent $eventstring $tenantIdstring $deliveryId從事件複製工作 id、事件型別、資料與時間戳記self不會拋出dispatch 所使用的靜態工廠
WebhookPayload::toJson以不轉義斜線序列化六欄位主體non-empty-string當事件資料無法 JSON 編碼時拋出 JsonExceptionJSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
WebhookPayload::toArray以關聯陣列回傳主體array<string, mixed>不會拋出時間戳記格式為 RFC 3339 延伸
WebhookPayload::signedTimestampUnix 秒數的事件時間,夾限為零或更大int<0, max>不會拋出X-NextPDF-Timestamp 送出並綁入 MAC
WebhookPayload::signstring $secret對基礎字串 {signedTimestamp}.{jsonBody} 計算 HMAC-SHA256non-empty-string(hex)當主體無法編碼時,透過 toJson() 拋出 JsonException以密碼學方式將時間戳記標頭綁定至主體
WebhookDelivery::__constructClientInterface $httpClientRequestFactoryInterface $requestFactoryStreamFactoryInterface $streamFactoryWebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy()?LoggerInterface $logger = null帶有空死信佇列的 PSR-18/PSR-17 傳遞引擎新的 WebhookDelivery不會拋出預設原則:5 次嘗試、1 s 基礎、300 s 上限
WebhookDelivery::deliverWebhookRegistration $registrationWebhookPayload $payload以逐次嘗試的 SSRF 出向驗證與指數退避 POST 已簽署的酬載bool當主體無法編碼時,在第一次嘗試前拋出 JsonException;否則不會拋出——false 代表酬載已被送入死信佇列僅在 2xx 回應時為 true
WebhookDelivery::deadLetters回傳所有已記錄的項目list<DeadLetterEntry>不會拋出記憶體中,範圍限於程序
WebhookDelivery::clearDeadLetters清空死信佇列void不會拋出不可逆;若需要重播請先匯出項目
WebhookRetryPolicy::__constructint $maxRetries = 5int $baseDelaySeconds = 1int $maxDelaySeconds = 300儲存原則的值新的 WebhookRetryPolicy未宣告 @throws;參數以 positive-int 記載$maxRetries 計數的是總嘗試次數
WebhookRetryPolicy::delayForAttemptint $attemptbaseDelaySeconds × 2^(attempt − 1),以 maxDelaySeconds 為上限positive-int不會拋出嘗試編號以 1 為起始
WebhookRetryPolicy::shouldRetryint $currentAttempt當目前嘗試低於最大值時為 truebool不會拋出最後一次嘗試之後會跳過等待
WebhookRetryPolicy::default5 次嘗試、1 s 基礎、300 s 上限self不會拋出靜態工廠;正式環境預設
WebhookRetryPolicy::aggressive10 次嘗試、2 s 基礎、600 s 上限self不會拋出針對關鍵端點的靜態工廠
DeadLetterEntry::__constructstring $idstring $registrationIdWebhookPayload $payloadint $attemptsstring $lastError?int $lastHttpStatusDateTimeImmutable $failedAtbool $replayed = false原封不動地儲存失敗紀錄新的 DeadLetterEntry未宣告 @throws;在 strict_types 下拋出 TypeErrorfinal readonly;null 的 $lastHttpStatus 代表傳輸失敗
DeadLetterEntry::markReplayed回傳一份 replayed = true 的副本self不會拋出相同 id;原始項目維持不變
public function __construct(
private readonly WebhookDelivery $delivery,
private readonly ?LoggerInterface $logger = null,
) {}
public function register(TenantContext $tenant, WebhookRegistration $registration): void
public function unregister(TenantContext $tenant, string $registrationId): bool
public function activeRegistrations(TenantContext $tenant): array
public function dispatch(TenantContext $tenant, JobEvent $event): int
public function __construct(
public string $id,
public string $tenantId,
public string $url,
public array $events,
public string $secret,
public bool $active = true,
public ?string $description = null,
) {}
public function subscribesTo(JobEventType $eventType): bool
public function deactivate(): self
public static function fromJobEvent(
JobEvent $event,
string $tenantId,
string $deliveryId,
): self
public function toJson(): string
public function toArray(): array
public function signedTimestamp(): int
public function sign(string $secret): string
public function __construct(
private readonly ClientInterface $httpClient,
private readonly RequestFactoryInterface $requestFactory,
private readonly StreamFactoryInterface $streamFactory,
private readonly WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(),
private readonly ?LoggerInterface $logger = null,
) {}
public function deliver(WebhookRegistration $registration, WebhookPayload $payload): bool
public function deadLetters(): array
public function clearDeadLetters(): void
public function __construct(
public int $maxRetries = 5,
public int $baseDelaySeconds = 1,
public int $maxDelaySeconds = 300,
) {}
public function delayForAttempt(int $attempt): int
public function shouldRetry(int $currentAttempt): bool
public static function default(): self
public static function aggressive(): self
public function __construct(
public string $id,
public string $registrationId,
public WebhookPayload $payload,
public int $attempts,
public string $lastError,
public ?int $lastHttpStatus,
public DateTimeImmutable $failedAt,
public bool $replayed = false,
) {}
public function markReplayed(): self
  • 登錄是逐租戶索引的。register() 會拒絕租戶識別碼與呼叫情境不符的登錄。unregister() 是一種軟停用:登錄會被替換為一份停用的副本,保留歷史的同時將其排除於未來的派送之外。
  • dispatch() 只會走訪呼叫租戶中訂閱了所派送事件型別的有效登錄。空的已訂閱事件清單代表訂閱全部。回傳值計數的是成功傳遞數。
  • 每次傳遞都是一個帶有 JSON 主體與五個標頭的 HTTP POST:Content-Type: application/jsonX-NextPDF-Signaturesha256=<hex>)、X-NextPDF-Timestamp(unix 秒數)、X-NextPDF-Delivery-IdX-NextPDF-Event
  • JSON 主體欄位為 delivery_idjob_idevent_typedatatimestamp(RFC 3339 延伸)與 tenant_id,以不轉義斜線序列化。事件型別值來自 nextpdf/core 中的 JobEventTypeprogresscompletedfailedcancelled
  • 簽章方案(於 3.1.0 變更,破壞性)。 HMAC-SHA256 的基礎字串是 {signedTimestamp}.{jsonBody},以登錄密鑰為金鑰——而非僅有主體。X-NextPDF-Timestamp 值是 MAC 的時間戳記部分,因此遭竄改或重播的時間戳記標頭會使簽章失效。
  • 接收方驗證:讀取 X-NextPDF-Timestamp 標頭 T;當 T 落在可接受的新鮮度視窗(例如 300 s)之外時予以拒絕;對原始接收的 bytes 重新計算 hash_hmac('sha256', T . '.' . rawBody, secret);剝除 sha256= 前綴後,以常數時間與標頭值比較。
  • 主體、簽章與傳遞 id 每次傳遞只計算一次,並在各次重試嘗試間維持不變。
  • SSRF 出向閘門。 每次嘗試前,目的地 URL 都會通過 Core 的 UrlValidator::validateExternalUrl() 閘門:僅限 HTTPS scheme;loopback、私有、保留、carrier-grade-NAT、cloud-metadata 與 IPv4 內嵌的 IPv6 轉換範圍皆被封鎖;主機名稱會經 DNS 解析(A 與 AAAA),無法解析的主機以 fail-closed 拒絕。被封鎖的 URL 絕不會送出:嘗試迴圈中止,酬載直接以 Blocked SSRF destination: 最後錯誤與 null HTTP 狀態送入死信佇列。
  • 逐次嘗試的結果分類:2xx 為成功並立即回傳;429 以外的 4xx 為終止,直接進入死信;其他所有結果——3xx、429、5xx 或傳輸例外——皆可重試,直到原則的總嘗試次數。
  • 退避為指數式:下一次嘗試前的等待為 baseDelaySeconds × 2^(attempt − 1),以 maxDelaySeconds 為上限。最後一次嘗試之後會跳過等待。
  • 當沒有任何嘗試成功時,DeadLetterEntry 會記錄一個唯一 id、登錄 id、原始酬載、嘗試次數(夾限至原則最大值)、最後一則錯誤訊息、最後的 HTTP 狀態(傳輸失敗或 SSRF 封鎖時為 null),以及失敗時間戳記。
  • 死信佇列在記憶體中,且範圍限於程序生命週期。markReplayed() 會產生一份帶標記的副本;它不會重送,且佇列會保留原始項目。
  • 空事件清單。 該登錄會收到每一種事件型別。當接收方不應看到所有事件時,請明確界定清單範圍。
  • 終止的 4xx 與傳輸失敗。 4xx 拒絕會記錄一個已填入的 lastHttpStatus;連線失敗則記錄 null。請用 null 來區分接收方拒絕與傳輸失敗。
  • SSRF 封鎖的目的地。 指向 HTTP、私有、loopback 或 metadata 位址的登錄會在第一次嘗試就以 Blocked SSRF destination: 錯誤與 null 狀態進入死信。不會發出任何對外請求。請修正 URL 後重新登錄。
  • 升級後的舊版接收方。 仍在驗證 3.1.0 之前僅主體 HMAC 的接收方,對 3.1.0 的傳遞會 fail closed。請將接收方遷移至 {timestamp}.{body} 基礎字串並消費 X-NextPDF-Timestamp
  • 無法編碼的事件資料。 toJson()sign() 會拋出 JsonException,並在任何嘗試發生之前從 deliver()dispatch() 向外傳播。
  • 同步阻塞。 deliver() 會在各次嘗試間就地 sleep。累積退避在 default 原則下達 15 s,在 aggressive 原則下約 17 分鐘。當接收方延遲不可信時,請自佇列 worker 派送。
  • 嘗試次數夾限。 已記錄的嘗試次數絕不會超過原則最大值,即使內部迴圈計數器在耗盡時會推進超過它。
  • 佇列成長與持久性。 死信佇列在程序內無界成長,並於重啟時消失。當需要持久重播時,請在呼叫 clearDeadLetters() 之前透過 deadLetters() 匯出項目並於外部持久化。
  • 重播由操作者驅動。 重新傳遞意指以項目的酬載再次呼叫 deliver()markReplayed() 只是在一份副本上記錄此事實。
  • DNS 重新綁定的殘留風險。 URL 會在每次嘗試時重新驗證,這縮小但未關閉重新綁定的視窗:PSR-18 抽象無法將連線釘定到已驗證的 IP。在此殘留風險重要之處,請加上網路層的出向控制。
  • 密鑰處理。 登錄密鑰是一項憑證。HMAC 只驗證完整性與來源——它不是機密性。請勿將接收方不應看到的資料放入事件酬載。

酬載簽署是透過 PHP 的 hash_hmac() 進行的 HMAC-SHA256,因此依賴主機的密碼學供應者。在 FIPS 受限的建置中,非核准的原語會在密碼學邊界失敗,而非降級。webhook 層不另加任何自有的密碼學原則。

  • 酬載驗證實作了 HMAC,即 FIPS PUB 198-1 §1 的金鑰雜湊訊息驗證碼,以 SHA-256 實例化。
  • 重播防護遵循 OWASP Cheat Sheet Series 的 webhook 安全指引:事件時間戳記以專屬標頭傳遞並植入簽章運算,因此遭竄改的時間戳記會驗證失敗。
  • 主體時間戳記使用 RFC 3339 延伸日期時間格式。由程式碼宣告:本頁未從 RAG 語料庫取得 RFC 3339。
  • 這些是根植於產品原始碼與所引條款的能力陳述。NextPDF 對此範圍不作任何一致性或認證主張。
  • 所有類別皆宣告 strict_types=1 且為 finalWebhookRegistrationWebhookPayloadWebhookRetryPolicyDeadLetterEntryfinal readonly,並帶有提升的公開屬性。
  • 此模組帶有 2.2.0@since 註解;時間戳記綁定的簽章方案是 3.1.0 中一項已記載的破壞性變更。
  • 傳遞引擎接受 PSR-18/PSR-17 抽象,因此一個 mock HTTP 用戶端即可離線演練完整的傳送、重試與死信路徑。logger 預設為 null;請在正式環境注入一個 PSR-3 logger,否則失敗只會透過回傳值浮現。
  • 接收方實作應使用 hash_equals() 進行簽章比較,並對 X-NextPDF-Timestamp 強制執行新鮮度視窗。
  • 建議的邊界測試:租戶不符的登錄、空事件清單的扇出、終止的 4xx、重試耗盡、SSRF 封鎖的 URL、對固定向量拒絕遭竄改時間戳記的簽章,以及死信嘗試次數的夾限。

本頁僅記載外部可觀察的行為與受支援的公開 API 範圍。內部命名空間路徑、輔助類別、機制表格、runbook 檔名與工單前綴皆不在範圍內。