{"record":{"id":"771fd64fcb3e67f0","repo":"gitroomhq/postiz-app","slug":"all-media-must-be-uploaded-through-our-upload-api","errorCode":null,"errorMessage":"All media must be uploaded through our upload API route and contain the domain: ${process.env.RESTRICT_UPLOAD_DOMAINS}","messagePattern":"All media must be uploaded through our upload API route and contain the domain: (.+?)","errorType":"http","errorClass":"HttpException","httpStatus":400,"severity":"error","filePath":"apps/backend/src/public-api/routes/v1/public.integrations.controller.ts","lineNumber":215,"sourceCode":"    Sentry.metrics.count('public_api-request', 1);\n    const body = await this._postsService.mapTypeToPost(\n      rawBody,\n      org.id,\n      rawBody?.type === 'draft' || true\n    );\n    body.type = rawBody.type;\n\n    if (\n      process.env.RESTRICT_UPLOAD_DOMAINS &&\n      body.posts.some((p) =>\n        p.value.some((a) =>\n          a.image.some(\n            (i) => i.path.indexOf(process.env.RESTRICT_UPLOAD_DOMAINS) === -1\n          )\n        )\n      )\n    ) {\n      throw new HttpException(\n        {\n          msg: `All media must be uploaded through our upload API route and contain the domain: ${process.env.RESTRICT_UPLOAD_DOMAINS}`,\n        },\n        400\n      );\n    }\n\n    // Server-side validation — same rules as the dashboard, surfaced as a\n    // readable 400 (see PostValidationExceptionFilter).\n    const validation = await this._postsService.validatePosts(\n      org.id,\n      body.posts\n    );\n\n    const fail = (item: (typeof validation)[number], error: string) => {\n      throw new PostValidationException({\n        provider: item.identifier,\n        name: item.name,","sourceCodeStart":197,"sourceCodeEnd":233,"githubUrl":"https://github.com/gitroomhq/postiz-app/blob/0f1647f7491a217d43eb5ae7a480484bdf0aff3e/apps/backend/src/public-api/routes/v1/public.integrations.controller.ts#L197-L233","documentation":"createPost rejects any image whose path does not contain the RESTRICT_UPLOAD_DOMAINS env value. When media domain restriction is enabled, all media referenced in posts must have been uploaded through the platform's own upload API route, so third-party/hotlinked media URLs are refused with a 400.","triggerScenarios":"POST /public/v1/posts with an image entry whose path is an external URL (e.g. https://images.unsplash.com/...) while the RESTRICT_UPLOAD_DOMAINS env var is set. The check is a substring indexOf on each image path, so any media not originating from the configured upload domain fails.","commonSituations":"RESTRICT_UPLOAD_DOMAINS set in the environment (often by default or copied from production .env) while the client hotlinks external images; custom self-hosted deployments forgetting to point the env at their own upload/CDN domain; clients migrating from an API that allowed arbitrary URLs.","solutions":["First upload the image via the public upload endpoint and use the returned URL in the post","If self-hosting, set RESTRICT_UPLOAD_DOMAINS to your own upload/CDN domain so platform-uploaded media passes the check","If you intentionally want external media allowed, unset/empty RESTRICT_UPLOAD_DOMAINS (only if your deployment's security model permits)","Ensure the configured domain matches exactly the domain in returned upload URLs (no scheme/port mismatch in the substring check)"],"exampleFix":"// before\n{ \"image\": [{ \"path\": \"https://images.example.com/cat.jpg\" }] }\n\n// after\nconst upload = await postiz.upload.fromUrl({ url: 'https://images.example.com/cat.jpg' });\n{ \"image\": [{ \"path\": upload.url }] } // served under RESTRICT_UPLOAD_DOMAINS","handlingStrategy":"validation","validationCode":"const domain = process.env.RESTRICT_UPLOAD_DOMAINS;\nconst paths = post.posts.flatMap(p => p.value.flatMap(v => v.image ?? []).map(i => i.path));\nif (domain && paths.some(p => !p.includes(domain))) {\n  // upload these through the API first, then swap in returned URLs\n}","typeGuard":"const isAllowedMediaPath = (path: string): boolean =>\n  !process.env.RESTRICT_UPLOAD_DOMAINS || path.includes(process.env.RESTRICT_UPLOAD_DOMAINS);","tryCatchPattern":"try {\n  await api.createPost(body);\n} catch (e) {\n  if (String(e?.response?.data?.msg).includes('RESTRICT_UPLOAD_DOMAINS')) {\n    // re-upload media via the upload route and retry with returned URLs\n  }\n}","preventionTips":["Always upload media through the platform upload API and reference returned URLs","Self-hosted: set RESTRICT_UPLOAD_DOMAINS to your own upload domain","Never hotlink third-party image URLs in post payloads"],"tags":["upload","domain-restriction","validation","env-var","public-api"],"backgroundTag":"media-domain-restriction","analyzedSha":"0f1647f7491a217d43eb5ae7a480484bdf0aff3e","analyzedAt":"2026-08-27T12:09:55.020Z","schemaVersion":2},"datasetVersion":"2026-08-27T13:17:12.746Z"}