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

  1. Verify the media URL in item.path is still reachable (curl -I with accept-encoding: identity)
  2. Re-upload or re-attach the media so a fresh URL is stored on the post
  3. If using signed URLs, extend expiry or regenerate them at publish time
  4. 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

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


AI-assisted analysis of gitroomhq/postiz-app@0f1647f749 (2026-08-27). Data as JSON: /api/errors/76c65285fa5984cb. Report an issue: GitHub.