From ca85fdedcb7244088c61813f8d58b0bc8d4f9617 Mon Sep 17 00:00:00 2001 From: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 10:19:55 +0000 Subject: [PATCH] fix: proxy direct APNs HTTP2 sessions --- src/infra/net/proxy/proxy-validation.test.ts | 32 ++++++++++++++++--- src/infra/net/proxy/proxy-validation.ts | 33 ++++++++++++++++++-- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/infra/net/proxy/proxy-validation.test.ts b/src/infra/net/proxy/proxy-validation.test.ts index df383dafe94..4c0402b9eef 100644 --- a/src/infra/net/proxy/proxy-validation.test.ts +++ b/src/infra/net/proxy/proxy-validation.test.ts @@ -467,7 +467,31 @@ describe("proxy validation", () => { }); }); - it("accepts APNs 403 reachability even when apns-id is unavailable", async () => { + it("accepts APNs 403 reachability with InvalidProviderToken when apns-id is unavailable", async () => { + const result = await runProxyValidation({ + config: { + enabled: true, + proxyUrl: "http://127.0.0.1:3128", + }, + env: {}, + allowedUrls: [], + deniedUrls: [], + apnsReachability: true, + apnsCheck: vi.fn().mockResolvedValue({ status: 403, apnsReason: "InvalidProviderToken" }), + }); + + expect(result.ok).toBe(true); + expect(result.checks).toEqual([ + { + kind: "apns", + url: "https://api.sandbox.push.apple.com", + ok: true, + status: 403, + }, + ]); + }); + + it("fails APNs reachability when bare 403 has no APNs proof", async () => { const result = await runProxyValidation({ config: { enabled: true, @@ -480,13 +504,13 @@ describe("proxy validation", () => { apnsCheck: vi.fn().mockResolvedValue({ status: 403 }), }); - expect(result.ok).toBe(true); + expect(result.ok).toBe(false); expect(result.checks).toEqual([ { kind: "apns", url: "https://api.sandbox.push.apple.com", - ok: true, - status: 403, + ok: false, + error: expect.stringContaining("InvalidProviderToken"), }, ]); }); diff --git a/src/infra/net/proxy/proxy-validation.ts b/src/infra/net/proxy/proxy-validation.ts index 4710df46086..82083716e47 100644 --- a/src/infra/net/proxy/proxy-validation.ts +++ b/src/infra/net/proxy/proxy-validation.ts @@ -10,6 +10,7 @@ export const DEFAULT_PROXY_VALIDATION_APNS_AUTHORITY = "https://api.sandbox.push const DEFAULT_PROXY_VALIDATION_TIMEOUT_MS = 5000; const DENIED_CANARY_HEADER = "x-openclaw-proxy-validation-canary"; +const APNS_REACHABILITY_REASON = "InvalidProviderToken"; export type ProxyValidationConfigSource = "override" | "config" | "env" | "missing" | "disabled"; @@ -62,6 +63,8 @@ export type ProxyValidationApnsCheckResult = { status: number; /** Present when the response originated from a real APNs server (Apple always returns this UUID). */ apnsId?: string; + /** APNs JSON error reason. InvalidProviderToken proves the invalid-token probe reached APNs. */ + apnsReason?: string; }; export type ProxyValidationApnsCheck = ( @@ -203,7 +206,31 @@ async function defaultProxyValidationApnsCheck({ timeoutMs, }: ProxyValidationApnsCheckParams): Promise { const result = await probeApnsHttp2ReachabilityViaProxy({ proxyUrl, authority, timeoutMs }); - return { status: result.status, apnsId: result.responseHeaders?.["apns-id"] }; + return { + status: result.status, + apnsId: result.responseHeaders?.["apns-id"], + apnsReason: parseApnsErrorReason(result.body), + }; +} + +function parseApnsErrorReason(body: string): string | undefined { + try { + const parsed: unknown = JSON.parse(body); + if (!parsed || typeof parsed !== "object") { + return undefined; + } + const reason = (parsed as { reason?: unknown }).reason; + return typeof reason === "string" && reason.trim() ? reason : undefined; + } catch { + return undefined; + } +} + +function hasApnsReachabilityProof(result: ProxyValidationApnsCheckResult): boolean { + if (result.apnsId) { + return true; + } + return result.status === 403 && result.apnsReason === APNS_REACHABILITY_REASON; } function normalizeTimeoutMs(value: number | undefined): number { @@ -422,13 +449,13 @@ async function runApnsReachabilityCheck(params: { authority: params.authority, timeoutMs: params.timeoutMs, }); - if (!result.apnsId && result.status !== 403) { + if (!hasApnsReachabilityProof(result)) { return { kind: "apns", url: params.authority, ok: false, error: - "APNs reachability check failed: response was not a 403 and did not include an apns-id header. " + + "APNs reachability check failed: response did not include an apns-id header or APNs InvalidProviderToken body. " + "The proxy may be intercepting the connection instead of tunneling it.", }; }