{"record":{"id":"b8e58b3d9fcce39c","repo":"gitroomhq/postiz-app","slug":"failed-to-upload-the-media-file","errorCode":null,"errorMessage":"Failed to upload the media file","messagePattern":"Failed to upload the media file","errorType":"exception","errorClass":"BadBody","httpStatus":null,"severity":"error","filePath":"libraries/nestjs-libraries/src/integrations/social/skool.provider.ts","lineNumber":276,"sourceCode":"      });\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(() => '{}'),\n          '{}',\n          'Failed to upload the media file'\n        );\n      }\n\n      fileIds.push(createFileResponse.file.id);\n    }\n\n    return fileIds.join(',');\n  }\n\n  async post(\n    id: string,\n    accessToken: string,\n    postDetails: PostDetails[],\n    integration: Integration","sourceCodeStart":258,"sourceCodeEnd":294,"githubUrl":"https://github.com/gitroomhq/postiz-app/blob/0f1647f7491a217d43eb5ae7a480484bdf0aff3e/libraries/nestjs-libraries/src/integrations/social/skool.provider.ts#L258-L294","documentation":"After downloading the media bytes, the provider PUTs them to the write_url Skool returned from its create-file step. If the PUT response is not OK, a BadBody error 'Failed to upload the media file' is thrown, deliberately preventing the post from publishing with an attachment id that has no stored bytes.","triggerScenarios":"Skool's write_url (presigned upload endpoint) rejects the PUT: expired signature, wrong Content-Type/Content-Length headers, body stream aborted mid-upload, or a 5xx from the storage host.","commonSituations":"Slow networks causing the presigned URL to expire before the streamed body finishes, content-type mismatch between the create-file metadata and the actual PUT headers, or transient Skool storage errors.","solutions":["Retry the post - transient presign/storage failures usually succeed on retry","Confirm the Content-Type sent in create-file matches the PUT header","Check contentLength accuracy; a wrong Content-Length causes a truncated stream rejection","Capture uploadResponse.text() in logs to see the storage host's reason"],"exampleFix":"// before\nif (!uploadResponse.ok) { throw new BadBody(...); }\n// after: retry the PUT once before failing\nlet uploadResponse = await doPut();\nif (!uploadResponse.ok) { await timer(1000); uploadResponse = await doPut(); }\nif (!uploadResponse.ok) { throw new BadBody(...); }","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { await uploadMediaToSkool(...); } catch (e) { if (e instanceof BadBody && /upload the media file/.test(e.message)) return retryOnce(); throw e; }","preventionTips":["Stream the PUT promptly after the create-file call so the write_url doesn't expire","Keep Content-Type consistent between metadata and PUT","Retry the whole upload flow once on storage 5xx"],"tags":["skool","media-upload","presigned-put","http"],"backgroundTag":"presigned-upload-rejected","analyzedSha":"0f1647f7491a217d43eb5ae7a480484bdf0aff3e","analyzedAt":"2026-08-27T12:09:55.020Z","schemaVersion":2},"datasetVersion":"2026-08-27T13:17:12.746Z"}