{"record":{"id":"76c65285fa5984cb","repo":"gitroomhq/postiz-app","slug":"failed-to-fetch-media-fileresponse-statustext-76c652","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/skool.provider.ts","lineNumber":260,"sourceCode":"          body: JSON.stringify({\n            file_name: fileName,\n            content_type: contentType,\n            content_length: contentLength,\n            content_disposition: '',\n            ref: '',\n            owner_id: userId,\n            large_thumbnail: false,\n          }),\n        }, 'create file record')\n      ).json();\n\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.write_url, {\n        method: 'PUT',\n        headers: {\n          'Content-Type': createFileResponse.content_type,\n          'Content-Length': String(contentLength),\n          'x-amz-acl': createFileResponse.acl,\n        },\n        body: fileResponse.body,\n        // Required by undici when streaming a request body.\n        duplex: 'half',\n      } as any);\n      // A rejected PUT would otherwise publish the post with an empty\n      // attachment - the file id exists but no bytes were stored.\n      if (!uploadResponse.ok) {\n        throw new BadBody(\n          this.identifier,\n          await uploadResponse.text().catch(() => '{}'),","sourceCodeStart":242,"sourceCodeEnd":278,"githubUrl":"https://github.com/gitroomhq/postiz-app/blob/0f1647f7491a217d43eb5ae7a480484bdf0aff3e/libraries/nestjs-libraries/src/integrations/social/skool.provider.ts#L242-L278","documentation":"While uploading an attachment to Skool, the provider first fetches the media from its stored URL (item.path) with an SSRF-safe dispatcher. If that fetch returns a non-OK status or an empty body, it throws 'Failed to fetch media: <statusText>'. This is the download stage of uploadMediaToSkool, not the Skool API call itself.","triggerScenarios":"The stored media URL (e.g. an S3/upload URL recorded on the post) returns 403/404/410, the file was deleted or its signed URL expired, the host returns 5xx, or the SSRF-safe dispatcher blocks an internal/private IP.","commonSituations":"Expired pre-signed S3 URLs, media deleted from storage, misconfigured public base URL pointing at an internal address, or a storage bucket whose objects are no longer public/signable.","solutions":["Verify the media URL in item.path is still reachable (curl -I with accept-encoding: identity)","Re-upload or re-attach the media so a fresh URL is stored on the post","If using signed URLs, extend expiry or regenerate them at publish time","Check that the storage endpoint is not resolving to a private IP that getSsrfSafeDispatcher() blocks"],"exampleFix":"// before\nconst fileResponse = await fetch(item.path, { headers: { 'accept-encoding': 'identity' } });\n// after: refresh/re-sign the media URL before upload\nconst freshUrl = await this.storage.presign(item.path);\nconst fileResponse = await fetch(freshUrl, { headers: { 'accept-encoding': 'identity' } });","handlingStrategy":"validation","validationCode":"// before uploading, confirm the media URL is fetchable\nconst head = await fetch(item.path, { method: 'HEAD' });\nif (!head.ok) throw new Error(`Media unavailable (${head.status}): ${item.path}`);","typeGuard":"const isFetchableMedia = (m: { path: string; mimetype?: string }) =>\n  typeof m?.path === 'string' && /^https?:\\/.\\//.test(m.path);","tryCatchPattern":"try { await uploadMediaToSkool(...); } catch (e) { if (/Failed to fetch media/.test(e.message)) reattachFreshMedia(); else throw e; }","preventionTips":["Regenerate presigned media URLs at publish time instead of storing long-lived ones","Run a HEAD check on media URLs when posts are scheduled","Monitor storage bucket expiry settings"],"tags":["skool","media-upload","storage","http"],"backgroundTag":"media-download-failed","analyzedSha":"0f1647f7491a217d43eb5ae7a480484bdf0aff3e","analyzedAt":"2026-08-27T12:09:55.020Z","schemaVersion":2},"datasetVersion":"2026-08-27T13:17:12.746Z"}