{"record":{"id":"a805abecde42a362","repo":"gitroomhq/postiz-app","slug":"failed-to-fetch-media-fileresponse-statustext-a805ab","errorCode":null,"errorMessage":"Failed to fetch media: ${fileResponse.statusText}","messagePattern":"Failed to fetch media: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"libraries/nestjs-libraries/src/integrations/social/whop.provider.ts","lineNumber":280,"sourceCode":"              Authorization: `Bearer ${accessToken}`,\n              'Content-Type': 'application/json',\n            },\n            body: JSON.stringify({\n              filename: fileName,\n            }),\n          },\n          'create file record'\n        )\n      ).json();\n\n      if (createFileResponse.upload_url) {\n        const fileResponse = await fetch(item.path, {\n          headers: { 'accept-encoding': 'identity' },\n          // @ts-ignore - undici-only option; blocks SSRF to internal IPs\n          dispatcher: getSsrfSafeDispatcher(),\n        });\n        if (!fileResponse.ok || !fileResponse.body) {\n          throw new Error(`Failed to fetch media: ${fileResponse.statusText}`);\n        }\n        const uploadResponse = await fetch(createFileResponse.upload_url, {\n          method: 'PUT',\n          headers: {\n            // Whop's own upload_headers win if they ever include a length.\n            'Content-Length': String(contentLength),\n            ...(createFileResponse.upload_headers || {}),\n          },\n          body: fileResponse.body,\n          // Required by undici when streaming a request body.\n          duplex: 'half',\n        } as any);\n        // A rejected PUT leaves the file pending forever - fail fast instead\n        // of burning the ~9 minute status poll below.\n        if (!uploadResponse.ok) {\n          throw new BadBody(\n            this.identifier,\n            await uploadResponse.text().catch(() => '{}'),","sourceCodeStart":262,"sourceCodeEnd":298,"githubUrl":"https://github.com/gitroomhq/postiz-app/blob/0f1647f7491a217d43eb5ae7a480484bdf0aff3e/libraries/nestjs-libraries/src/integrations/social/whop.provider.ts#L262-L298","documentation":"While uploading media to Whop, the GET of the media file failed (non-ok status or empty body) and it throws with the fetch statusText. Fetch uses an SSRF-safe dispatcher, so internal/unreachable hosts also fail here.","triggerScenarios":"GET on item.path returns 403/404 (expired presigned URL), DNS/network failure, or the URL points at a private IP which the SSRF-safe dispatcher blocks.","commonSituations":"Expired media URLs in pending posts; media host temporarily down; self-hosted storage on an internal hostname unreachable from the worker; presigned URL signed for a different request (e.g. GET disallowed).","solutions":["curl the media URL from the worker machine to confirm reachability and a 200","Re-add the media to the post so a fresh URL is generated","If self-hosting, ensure the storage host is publicly resolvable/reachable from the orchestrator workers","Check statusText/body in logs for the exact HTTP failure and fix that (403 vs 404 vs DNS)"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"const testFetch = await fetch(mediaUrl, { method: 'HEAD', dispatcher: getSsrfSafeDispatcher() } as any);\nif (!testFetch.ok) { /* refresh media URL or fail before upload */ }","typeGuard":null,"tryCatchPattern":"catch (e) { if (e instanceof Error && e.message.startsWith('Failed to fetch media')) { /* transient network: retry with backoff; permanent if 403/404 */ } throw e; }","preventionTips":["Keep media hosts publicly reachable from workers (SSRF-safe dispatcher blocks internal IPs)","Probe media URLs before scheduling-dependent uploads","Re-upload media when presigned URLs are near expiry"],"tags":["whop","media-download","fetch-failed","ssrf"],"backgroundTag":"media-url-unfetchable","analyzedSha":"0f1647f7491a217d43eb5ae7a480484bdf0aff3e","analyzedAt":"2026-08-27T12:09:55.020Z","schemaVersion":2},"datasetVersion":"2026-08-27T13:17:12.746Z"}