RocketChat/Rocket.Chat · warning · Meteor.Error
error-invalid-file-uploaded
error-invalid-file-uploaded
Error message
Invalid file uploaded
What it means
Thrown inside the inbound SMS webhook (POST /api/v1/livechat/sms-incoming/:service) when the server downloads an MMS/SMS media attachment and the provider responds with a non-200 status or an empty body. The fetch goes through Rocket.Chat's serverFetch with SSRF protection, so a host rejected by the SSRF_Allowlist setting also surfaces here. Note: in the current code this throw is caught at the call site (apps/meteor/server/api/v1/omnichannel/sms.ts:223), logged as 'Attachment upload failed', and the message is still delivered with a placeholder 'Attachment upload failed' attachment — so you normally see it in logs, not as an HTTP error to the SMS provider.
Solutions
- Add the SMS provider's media host (e.g. *.twilionondialog.com / your gateway domain) to Administration -> Settings -> General -> SSRF Allowlist (SSRF_Allowlist)
- Curl the exact media URL from the Rocket.Chat server host to confirm it returns 200 with bytes
- Process webhooks promptly — provider media URLs are time-limited; avoid long queue/retry delays
- Check the server log for the 'SMS' logger entry 'Attachment upload failed' to see the underlying fetch error
Defensive patterns
Strategy: retry
Validate before calling
// If you control the SMS gateway, pre-validate the media URL the way the server will fetch it:
const check = await fetch(mediaUrl, { method: 'HEAD' });
if (check.status !== 200) log.warn('media not fetchable yet', mediaUrl); Try / catch
// The webhook itself is server-side; guard as the SMS integrator by re-fetching media promptly:
for (let attempt = 1; attempt <= 3; attempt++) {
try { return await downloadMedia(url); } // 200 + non-empty body required
catch (e) { await backoff(attempt); } // provider CDNs recover / URLs unblock after allowlist change
}
throw new Error('media download failed after retries'); Prevention
- Pre-register every media host your SMS providers use in SSRF_Allowlist
- Fetch attachments immediately on webhook receipt — media URLs expire
- Monitor the 'Attachment upload failed' log pattern to catch silent attachment loss
When it happens
Trigger: An inbound SMS with a media URL that returns 403/404 (Twilio media links expire after a while), a media host not present in the SSRF_Allowlist setting so serverFetch blocks it, a provider returning 200 with a zero-byte body, or a media URL pointing at an internal/private host that the SSRF guard forbids.
Common situations: Replaying or retrying old Twilio webhooks whose media URLs have expired; a self-hosted SMS gateway (e.g. a custom service integration) serving media from an internal domain; workspaces that locked down SSRF_Allowlist after a security review and forgot the SMS provider's media CDN.
Related errors
- error-invalid-visitor
- Attachments should be Array, ignoring value
- error-invalid-webhook-response
- External service response does not match expected format
- File name is required
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/309a2e8fd9de79c3.
Report an issue: GitHub.
Appendix: source
Thrown at apps/meteor/server/api/v1/omnichannel/sms.ts:40
import type { ILivechatMessage } from '../../../lib/omnichannel/localTypes';
import { sendMessage } from '../../../lib/omnichannel/messages';
import { createRoom } from '../../../lib/omnichannel/rooms';
import { settings } from '../../../settings';
const logger = new Logger('SMS');
const getUploadFile = async (details: Omit<IUpload, '_id' | '_updatedAt'>, fileUrl: string) => {
const response = await fetch(fileUrl, {
ignoreSsrfValidation: false,
allowList: settings.get<string>('SSRF_Allowlist'),
});
const content = Buffer.from(await response.arrayBuffer());
const contentSize = content.length;
if (response.status !== 200 || contentSize === 0) {
throw new Meteor.Error('error-invalid-file-uploaded', 'Invalid file uploaded');
}
const fileStore = FileUpload.getStore('Uploads');
return fileStore.insert({ ...details, size: contentSize }, content);
};
const defineDepartment = async (idOrName?: string) => {
if (!idOrName || idOrName === '') {
return;
}
const department = await LivechatDepartment.findOneByIdOrName(idOrName, { projection: { _id: 1 } });
return department?._id;
};
const defineVisitor = async (smsNumber: string, targetDepartment?: string) => {
const visitor = await LivechatVisitors.findOneVisitorByPhone(smsNumber);View on GitHub (pinned to b2c16d5842)