Skip to content

fix(cdn): preserve response headers in CDN upload (#149)#160

Open
DraixAgent wants to merge 1 commit into
Tencent:mainfrom
DraixAgent:feature/fix-149-cdn-headers
Open

fix(cdn): preserve response headers in CDN upload (#149)#160
DraixAgent wants to merge 1 commit into
Tencent:mainfrom
DraixAgent:feature/fix-149-cdn-headers

Conversation

@DraixAgent
Copy link
Copy Markdown

Summary

Switches the Weixin CDN upload transport from global fetch to a small node:https / node:http POST helper (postBufferRaw in src/cdn/cdn-transport.ts). This preserves the raw response headers as Node sees them on the wire, so x-encrypted-param survives in the live OpenClaw message-tool execution context.

Fixes #149

Problem

When sending image / file / video media via the live OpenClaw message tool path, the Weixin CDN POST returned HTTP 200 but global fetch exposed an empty Headers object, so src/cdn/cdn-upload.ts couldn't read x-encrypted-param and the send failed with:

CDN upload response missing x-encrypted-param header

The same CDN flow worked correctly from:

  • the delivery-recovery path (after Gateway restart),
  • a standalone Node script,

which strongly suggested the issue was something about how global fetch was wired in the live tool execution context, not the CDN itself.

Root-cause investigation

Before changing the transport I looked for the actual source of the empty-Headers view in this repo:

  • grep for setGlobalDispatcher, MockAgent, ProxyAgent, HttpsProxyAgent, globalThis.fetch, global.fetch, custom Agent/interceptor/dispatcher configuration → nothing in this plugin overrides global fetch or installs a custom undici dispatcher.

  • Walked both call sites that reach uploadBufferToCdn:

    • Live message tool path: channel.tsmessaging/send-media.tscdn/upload.tscdn/cdn-upload.ts
    • Delivery-recovery path: same sendWeixinMediaFile entry point, also routes through cdn/cdn-upload.ts

    Both paths converge on the same function, so the difference can't be inside this plugin's code — it has to be in whatever runtime/interceptor is active around the live message-tool path (likely on the OpenClaw core side, outside this repo).

Given the symptom (response status visible, response headers empty) and that the recovery + standalone flows both work, the most plausible cause is something in the host runtime (a wrapped fetch, a logging/observability interceptor, or an undici dispatcher) stripping or rewriting the response headers view for that one execution context. Locating and fixing that in OpenClaw core is out of scope for this plugin PR.

So this PR implements the workaround the reporter validated: use Node's built-in HTTPS transport for the CDN POST only, which sees the raw on-the-wire headers regardless of what's wrapped around fetch in the host process. The rest of the plugin (which calls non-CDN APIs) continues to use fetch.

Fix

  • New src/cdn/cdn-transport.ts exports postBufferRaw(url, body) which POSTs a buffer via node:https (or node:http for http:// URLs) and returns { status, getHeader, text } — a minimal, case-insensitive view over Node's raw response.
  • src/cdn/cdn-upload.ts swaps the single fetch(...) call inside uploadBufferToCdn for postBufferRaw(...). All retry / status-class / error-handling / header-parsing logic is unchanged. Public API of uploadBufferToCdn is unchanged.
  • The transport is its own module so tests can mock it cleanly without touching the upload logic.

Affected files:

  • src/cdn/cdn-transport.ts (new)
  • src/cdn/cdn-upload.ts
  • src/cdn/cdn-upload.test.ts
  • src/cdn/upload.test.ts

Tests

Existing CDN tests are preserved (rewritten in upload.test.ts to mock postBufferRaw instead of fetch, since cdn-upload.ts no longer touches fetch). New tests cover the bug directly and use a real loopback node:http server, not just mocks — mocking fetch cannot catch the regression in #149 because the bug is in the transport layer, not the call site.

Coverage added in src/cdn/cdn-upload.test.ts:

  1. Live image send via message tool — CDN returns 200 + x-encrypted-param; the new transport surfaces the header correctly.
  2. Delivery-recovery image path — same end-to-end success, asserting no regression on the path that already worked.
  3. Standalone CDN upload — direct invocation succeeds end-to-end.
  4. CDN returns error — server returns 500 + x-error-message on every retry; we propagate the server message (e.g. "cdn unavailable"), not the misleading "x-encrypted-param missing" wording.
  5. Large image > 1MB — 1.5 MB payload uploads successfully via the new transport (chunked transmission still works end-to-end).

Plus regression coverage for the transport itself:

Plus preserved behavioral tests on uploadBufferToCdn (retries on 5xx, immediate fail on 4xx, missing header after all retries, x-error-message priority for 4xx, missing URL params guard).

Total: 15 tests in cdn-upload.test.ts (was 8), 10 tests in upload.test.ts (was 8), all passing locally.

Validation

$ npx vitest run src/cdn/
 ✓ src/cdn/upload.test.ts      (10 tests)
 ✓ src/cdn/cdn-upload.test.ts  (15 tests)
 Test Files  2 passed (2)
      Tests  25 passed (25)

$ npx tsc --noEmit
(clean)

Full suite: 1 pre-existing unrelated failure in src/auth/pairing.test.ts on upstream/main (confirmed by running the same test on a clean checkout) — out of scope for this PR.

Notes

  • This PR deliberately keeps fetch for everything except the CDN POST. pic-decrypt.ts (CDN downloads) doesn't depend on response headers in this way, and api.ts talks to the Weixin JSON API, which is unaffected by the observed bug.
  • If the OpenClaw-core-side interceptor that strips fetch response headers ever gets identified and fixed, this transport can keep coexisting safely; it doesn't change the wire protocol, just sidesteps the broken view.

…encent#149)

Switch the Weixin CDN upload transport from global fetch to a small
node:https / node:http POST helper (postBufferRaw in src/cdn/cdn-transport.ts).
This preserves the raw response headers as Node sees them on the wire, so
x-encrypted-param survives in the live OpenClaw message-tool execution
context.

In that context the global fetch implementation was exposing an empty
Headers object on 200 OK CDN responses, while the same flow worked from
the delivery-recovery path and from standalone Node scripts. The result
was live image / file / video sends failing immediately with
"CDN upload response missing x-encrypted-param header".

The new transport is its own module so tests can mock it cleanly, and so
the retry/status/header-parsing logic in cdn-upload.ts stays unchanged.

Fixes Tencent#149
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CDN upload via global fetch exposes empty headers in live message-tool path

1 participant