tonhowtf/omniget · warning · Error
ERR_TOO_MANY_ATTACHMENTS
ERR_TOO_MANY_ATTACHMENTS
Error message
ERR_TOO_MANY_ATTACHMENTS:${limits.maxAttachments} What it means
Raised by extract_graphql_media when Twitter's GraphQL response contains a tombstone entry whose reason is "NsfwLoggedOut" or whose tombstone text starts with "Age-restricted". The tweet exists but is age-gated, and the logged-out/unauthenticated session used for scraping is not allowed to see it, so the extractor aborts early with this message instead of trying to parse media that isn't there.
Solutions
- Authenticate the Twitter session (valid auth_token/ct0 cookies of an age-verified account) before fetching the tweet
- Mark the post as age-restricted/skippable in the caller instead of treating it as a hard failure
- Try the syndication API fallback (extract_syndication_media), which sometimes serves media for sensitive tweets without auth
- Retry with a different account/session if the current one is not age-verified
Example fix
// before
if reason == "NsfwLoggedOut" || tombstone_text.starts_with("Age-restricted") {
return Err(anyhow!("Age-restricted content"));
}
// after
if reason == "NsfwLoggedOut" || tombstone_text.starts_with("Age-restricted") {
return Err(TwitterError::AgeRestricted { tweet_id }); // typed error callers can match on and skip
} Defensive patterns
Strategy: try-catch
Validate before calling
// Before fetching, ensure the session is authenticated with an age-verified account
if !session.has_auth_cookies() {
return Err("age-gated tweets require an authenticated, age-verified session");
} Type guard
function isAgeRestrictedTombstone(result) {
return result?.__typename === 'TweetTombstone' &&
(result?.tombstone?.reason === 'NsfwLoggedOut' ||
String(result?.tombstone?.text ?? '').startsWith('Age-restricted'));
} Try / catch
match tweet.extract_graphql_media(id) {
Err(e) if e.to_string() == "Age-restricted content" => skip(id, Reason::AgeRestricted),
Err(e) => return Err(e),
Ok(media) => Ok(media),
} Prevention
- Use authenticated, age-verified Twitter cookies for all fetches
- Check a tweet's sensitive-content flag from metadata before requesting full media
- Classify age-restriction as a skippable outcome, not a pipeline failure
- Monitor auth cookie expiry and refresh sessions proactively
When it happens
Trigger: Calling extract_graphql_media (via the Twitter GraphQL tweet-detail flow) on a tweet_id whose result is a TweetTombstone with reason == "NsfwLoggedOut", or whose tombstone text begins with "Age-restricted" — i.e. the API returns a tombstone object instead of a "Tweet"/"TweetWithVisibilityResults" typename.
Common situations: Scraping NSFW/sensitive-media tweets while logged out; an account cookie or auth_token that expired so the session degrades to logged-out; tweets flagged sensitive by the author (sensitive_content flag) that require an age-verified logged-in session; region/age restrictions on the calling IP.
Related errors
- Age-restricted content
- HLS nao e suportado neste navegador
- refresh returned no audio format
- post indisponivel
- Age-restricted content
AI-assisted analysis of tonhowtf/omniget@8600b91f42 (2026-09-12).
Data as JSON: /api/errors/7e4b26df2025bb50.
Report an issue: GitHub.
Appendix: source
Thrown at src/lib/stores/omnidisc-store.svelte.ts:2577
function clearUploads(channelId: string) {
for (const upload of uploadsByChannel[channelId] ?? []) {
if (upload.previewUrl) URL.revokeObjectURL(upload.previewUrl);
}
const next = { ...uploadsByChannel };
delete next[channelId];
uploadsByChannel = next;
}
export async function attachFile(
channelId: string,
file: { path: string; name?: string; previewUrl?: string },
): Promise<string | null> {
const instance = instanceForChannel(channelId);
if (!instance || isDemo(instance.id)) return null;
const limits = await getUploadLimits(instance.id);
const already = uploadsByChannel[channelId] ?? [];
if (already.length >= limits.maxAttachments) {
throw new Error(`ERR_TOO_MANY_ATTACHMENTS:${limits.maxAttachments}`);
}
const encrypted = isEncryptedChannel(channelId);
// Rust owns the size check: it reads the file anyway and knows the instance
// limit, so the answer arrives before the first byte moves.
const started = await invoke<{ id: string; size: number; name: string }>(
"omnidisc_upload_start",
{ url: instance.url, channelId, path: file.path, encrypt: encrypted },
);
putUpload({
id: started.id,
channelId,
name: file.name ?? started.name,
path: file.path,
sent: 0,
total: started.size,
state: "preparing",
encrypted,
previewUrl: file.previewUrl,View on GitHub (pinned to 8600b91f42)