gitroomhq/postiz-app · error · Error
Failed to fetch media: ${fileResponse.statusText}
Error message
Failed to fetch media: ${fileResponse.statusText} What it means
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.
Source
Thrown at libraries/nestjs-libraries/src/integrations/social/skool.provider.ts:260
body: JSON.stringify({
file_name: fileName,
content_type: contentType,
content_length: contentLength,
content_disposition: '',
ref: '',
owner_id: userId,
large_thumbnail: false,
}),
}, 'create file record')
).json();
const fileResponse = await fetch(item.path, {
headers: { 'accept-encoding': 'identity' },
// @ts-ignore - undici-only option; blocks SSRF to internal IPs
dispatcher: getSsrfSafeDispatcher(),
});
if (!fileResponse.ok || !fileResponse.body) {
throw new Error(`Failed to fetch media: ${fileResponse.statusText}`);
}
const uploadResponse = await fetch(createFileResponse.write_url, {
method: 'PUT',
headers: {
'Content-Type': createFileResponse.content_type,
'Content-Length': String(contentLength),
'x-amz-acl': createFileResponse.acl,
},
body: fileResponse.body,
// Required by undici when streaming a request body.
duplex: 'half',
} as any);
// A rejected PUT would otherwise publish the post with an empty
// attachment - the file id exists but no bytes were stored.
if (!uploadResponse.ok) {
throw new BadBody(
this.identifier,
await uploadResponse.text().catch(() => '{}'),View on GitHub (pinned to 0f1647f749)
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
Example fix
// before
const fileResponse = await fetch(item.path, { headers: { 'accept-encoding': 'identity' } });
// after: refresh/re-sign the media URL before upload
const freshUrl = await this.storage.presign(item.path);
const fileResponse = await fetch(freshUrl, { headers: { 'accept-encoding': 'identity' } }); Defensive patterns
Strategy: validation
Validate before calling
// before uploading, confirm the media URL is fetchable
const head = await fetch(item.path, { method: 'HEAD' });
if (!head.ok) throw new Error(`Media unavailable (${head.status}): ${item.path}`); Type guard
const isFetchableMedia = (m: { path: string; mimetype?: string }) =>
typeof m?.path === 'string' && /^https?:\/.\//.test(m.path); Try / catch
try { await uploadMediaToSkool(...); } catch (e) { if (/Failed to fetch media/.test(e.message)) reattachFreshMedia(); else throw e; } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Failed to upload the media file
- Failed to fetch media: ${fileResponse.statusText}
- Reddit media upload did not return a location
- Could not determine the media size for upload
- Could not determine the media size for upload
AI-assisted analysis of gitroomhq/postiz-app@0f1647f749 (2026-08-27).
Data as JSON: /api/errors/76c65285fa5984cb.
Report an issue: GitHub.