Spaces:
Running
fix: CF Worker must re-POST body on X's api.twitter.com→api.x.com redirect
Browse filesRoot 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>
- cloudflare-proxy-setup.py +24 -7
- health-server.js +29 -0
|
@@ -147,19 +147,36 @@ async function handleRequest(request) {{
|
|
| 147 |
headers.delete("x-target-host");
|
| 148 |
headers.delete("x-proxy-key");
|
| 149 |
|
| 150 |
-
//
|
| 151 |
-
//
|
| 152 |
-
//
|
| 153 |
-
//
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
method: request.method,
|
| 156 |
headers,
|
| 157 |
-
body:
|
| 158 |
redirect: "manual",
|
| 159 |
}});
|
| 160 |
|
| 161 |
try {{
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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_token → api.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 |
}}
|
|
@@ -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");
|