{"record":{"id":"866684ae6b7200cc","repo":"gitroomhq/postiz-app","slug":"failed-to-fetch-media-fileresponse-statustext","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/mastodon.provider.ts","lineNumber":188,"sourceCode":"    instanceUrl: string,\n    fileUrl: string,\n    accessToken: string,\n    alt?: string\n  ) {\n    // The file is streamed straight from storage into the Mastodon upload\n    // request instead of being buffered in memory. Both requests keep the\n    // SSRF-safe dispatcher because the URLs are not fully under our control,\n    // and the whole request is rebuilt per attempt by runStreamedUpload since\n    // a consumed stream can't be replayed.\n    const media = await this.runStreamedUpload<{ id: string }>(async () => {\n      const fileResponse = await fetch(fileUrl, {\n        // identity encoding so content-length matches the streamed bytes\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 contentLength = Number(\n        fileResponse.headers.get('content-length') || 0\n      );\n\n      const form = new FormDataUpload();\n      form.append('file', Readable.fromWeb(fileResponse.body as any), {\n        filename: fileUrl.split('/').pop()?.split('?')[0] || 'file',\n        ...(contentLength ? { knownLength: contentLength } : {}),\n      });\n      if (alt) {\n        form.append('description', alt);\n      }\n\n      const mediaResponse = await fetch(`${instanceUrl}/api/v1/media`, {\n        method: 'POST',\n        headers: {\n          ...form.getHeaders(),","sourceCodeStart":170,"sourceCodeEnd":206,"githubUrl":"https://github.com/gitroomhq/postiz-app/blob/0f1647f7491a217d43eb5ae7a480484bdf0aff3e/libraries/nestjs-libraries/src/integrations/social/mastodon.provider.ts#L170-L206","documentation":"MastodonProvider.uploadFile fetches the media bytes from storage before uploading to Mastodon; this error means that intermediate download returned a non-ok response or empty body. The message carries the HTTP statusText of the failed fetch.","triggerScenarios":"fetch(media.url) (the uploaded file URL) returns !ok or no body — 404/403 on the storage URL, expired signed URL, or proxy returning an error page.","commonSituations":"Expired S3/cloud signed URLs, deleted media library file, misconfigured storage/CDN, or wrong public backend URL in env making media URLs unreachable.","solutions":["Verify the media file URL is publicly accessible (curl it from the server)","Check storage config (S3 bucket policy / signed URL expiry / BACKEND_URL env)","Re-add the media to the post from the media library and retry"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"const res = await fetch(media.url, { method: 'HEAD' });\nif (!res.ok) throw new Error(`Media URL not reachable (${res.status})`);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Verify media URLs resolve publicly before scheduling","Keep storage signed-URL TTL longer than max scheduling delay"],"tags":["mastodon","media-download","storage"],"backgroundTag":"media-download-failed","analyzedSha":"0f1647f7491a217d43eb5ae7a480484bdf0aff3e","analyzedAt":"2026-08-27T12:09:55.020Z","schemaVersion":2},"datasetVersion":"2026-08-27T13:17:12.746Z"}