somratpro Claude Sonnet 4.6 commited on
Commit
062df28
·
1 Parent(s): 86ec115

fix: CF Worker must re-POST body on X's api.twitter.com→api.x.com redirect

Browse files

Root cause: X redirects OAuth-signed POST /oauth/access_token from
api.twitter.com to api.x.com (confirmed by domain=.x.com in error
response cookies vs domain=.twitter.com for unauthenticated requests).
CF Worker redirect:"follow" converts POST→GET per fetch spec, losing the
OAuth-signed body → X receives unsigned GET → returns 500 HTML.

Fix (cloudflare-proxy-setup.py): buffer the request body as ArrayBuffer,
use redirect:"manual", then manually follow 3xx hops re-POSTing the same
body (up to 5 hops). This preserves method + OAuth signature on redirect.

Also add /app/redeploy-cf-worker endpoint to health-server.js: forces
Worker redeploy without a full Space restart by unsetting CLOUDFLARE_PROXY_URL
from subprocess env (bypassing the setup script's short-circuit check).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (2) hide show
  1. cloudflare-proxy-setup.py +24 -7
  2. health-server.js +29 -0
cloudflare-proxy-setup.py CHANGED
@@ -147,19 +147,36 @@ async function handleRequest(request) {{
147
  headers.delete("x-target-host");
148
  headers.delete("x-proxy-key");
149
 
150
- // Use redirect:"manual" so POST bodies are NOT silently dropped when X
151
- // returns a 3xx (fetch spec converts POSTGET on 301/302 redirect, losing
152
- // the OAuth-signed body and causing a signature mismatch → 500 from X).
153
- // The Node.js client receives the raw 3xx and handles it with body intact.
154
- const proxiedRequest = new Request(targetUrl, {{
 
 
 
 
 
 
155
  method: request.method,
156
  headers,
157
- body: request.body,
158
  redirect: "manual",
159
  }});
160
 
161
  try {{
162
- return await fetch(proxiedRequest);
 
 
 
 
 
 
 
 
 
 
 
163
  }} catch (error) {{
164
  return new Response(`Proxy Error: ${{error.message}}`, {{ status: 502 }});
165
  }}
 
147
  headers.delete("x-target-host");
148
  headers.delete("x-proxy-key");
149
 
150
+ // Buffer the entire request body so we can replay it on redirects.
151
+ // X redirects api.twitter.com/oauth/access_tokenapi.x.com with a 3xx.
152
+ // The default redirect:"follow" converts POST→GET (loses OAuth body → 500).
153
+ // redirect:"manual" returns a 3xx the Node.js client can't handle.
154
+ // Solution: buffer body, intercept 3xx manually, re-POST with same body.
155
+ let bodyBuffer = null;
156
+ if (request.body) {{
157
+ try {{ bodyBuffer = await request.arrayBuffer(); }} catch(_) {{}}
158
+ }}
159
+
160
+ const makeReq = (url) => new Request(url, {{
161
  method: request.method,
162
  headers,
163
+ body: bodyBuffer,
164
  redirect: "manual",
165
  }});
166
 
167
  try {{
168
+ let response = await fetch(makeReq(targetUrl));
169
+ let hops = 0;
170
+ // Follow 3xx redirects preserving method + body (max 5 hops).
171
+ while (hops < 5 && (response.status === 301 || response.status === 302 ||
172
+ response.status === 307 || response.status === 308)) {{
173
+ const location = response.headers.get("location");
174
+ if (!location) break;
175
+ hops++;
176
+ const next = new URL(location, targetUrl).toString();
177
+ response = await fetch(makeReq(next));
178
+ }}
179
+ return response;
180
  }} catch (error) {{
181
  return new Response(`Proxy Error: ${{error.message}}`, {{ status: 502 }});
182
  }}
health-server.js CHANGED
@@ -1775,6 +1775,35 @@ const server = http.createServer((req, res) => {
1775
  return;
1776
  }
1777
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1778
  // ── /app/test-twitter — raw connectivity probe to api.twitter.com ────────
1779
  if (pathname === "/app/test-twitter") {
1780
  const https = require("https");
 
1775
  return;
1776
  }
1777
 
1778
+ // ── /app/redeploy-cf-worker — force Cloudflare Worker redeploy ───────────
1779
+ // Strips CLOUDFLARE_PROXY_URL from subprocess env so the setup script
1780
+ // bypasses the short-circuit and deploys the updated Worker template.
1781
+ if (pathname === "/app/redeploy-cf-worker") {
1782
+ const { execFile } = require("child_process");
1783
+ const subEnv = { ...process.env };
1784
+ delete subEnv.CLOUDFLARE_PROXY_URL;
1785
+ delete subEnv.CLOUDFLARE_PROXY_SECRET;
1786
+ execFile("python3", ["/opt/postiz-sync.py", "--version"], { env: subEnv }, () => {}); // no-op warm-up
1787
+ execFile("python3", ["/opt/cloudflare-proxy-setup.py"], { env: subEnv, timeout: 60000 },
1788
+ (err, stdout, stderr) => {
1789
+ const lines = [
1790
+ "Redeploying Cloudflare Worker with updated template...",
1791
+ "",
1792
+ "=== stdout ===",
1793
+ stdout || "(empty)",
1794
+ "=== stderr ===",
1795
+ stderr || "(empty)",
1796
+ err ? "=== exec error ===\n" + err.message : "=== done ===",
1797
+ "",
1798
+ "Worker updated. Try connecting X now.",
1799
+ ];
1800
+ res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1801
+ res.end(lines.join("\n"));
1802
+ }
1803
+ );
1804
+ return;
1805
+ }
1806
+
1807
  // ── /app/test-twitter — raw connectivity probe to api.twitter.com ────────
1808
  if (pathname === "/app/test-twitter") {
1809
  const https = require("https");