tldraw/tldraw · error · StatusError
e instanceof Error ? e.message : String(e)
Error message
e instanceof Error ? e.message : String(e)
What it means
Thrown by POST /app/admin/mcp-friends-and-family when parseFriendsAndFamilyEmails throws during parsing. The catch wraps the parser's error message into a StatusError(400) so the caller sees exactly which entry was invalid. The message is dynamic — it reflects whatever the parser rejected (bad format, duplicate, empty entry).
Source
Thrown at apps/dotcom/sync-worker/src/adminRoutes.ts:271
if (enabled !== undefined) update.enabled = enabled
if (percentage !== undefined) update.percentage = percentage
await setFeatureFlag(env, flag as FeatureFlagKey, update)
return json({ success: true, flag, ...update })
})
.get('/app/admin/mcp-friends-and-family', async (_req, env) => {
return json({ entries: await getFriendsAndFamilyList(env) })
})
.post('/app/admin/mcp-friends-and-family', async (req, env) => {
const body: any = await req.json()
// Parsing before resolving means a typo is rejected at the point someone can still fix it,
// rather than sitting in the list looking like it grants access while matching nothing.
let emails: string[]
try {
emails = parseFriendsAndFamilyEmails(body?.entries)
} catch (e) {
throw new StatusError(400, e instanceof Error ? e.message : String(e))
}
const entries = await resolveFriendsAndFamilyUsers(env, emails)
await setFriendsAndFamilyList(env, entries)
return json({ success: true, entries })
})
.post('/app/admin/create_legacy_file', async (_res, env) => {
const slug = uniqueId()
await getRoomDurableObject(env, slug).__admin__createLegacyRoom(slug)
return json({ slug })
})
.post('/app/admin/hard_delete_file/:fileId', async (res, env) => {
const fileId = res.params.fileId
assert(typeof fileId === 'string', 'fileId is required')
const pg = createPostgresConnectionPool(env, '/app/admin/hard_delete_file')
const file = await pg.selectFrom('file').where('id', '=', fileId).selectAll().executeTakeFirst()
if (!file) {View on GitHub (pinned to b31086b447)
Solutions
- Inspect the wrapped message — it names the specific invalid entry.
- Normalize entries to plain email strings before posting; strip names, angle brackets, and whitespace.
- Validate each entry with a standard email regex client-side before submission.
Defensive patterns
Strategy: validation
Validate before calling
function parseEmails(entries: unknown): string[] {
const arr = Array.isArray(entries) ? entries : [entries]
return arr.map((e) => {
if (typeof e !== 'string') throw new Error(`Entry is not a string: ${JSON.stringify(e)}`)
const trimmed = e.trim()
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(trimmed)) throw new Error(`Invalid email: ${trimmed}`)
return trimmed.toLowerCase()
})
} Type guard
function isEmail(value: unknown): value is string {
return typeof value === 'string' && /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value.trim())
} Try / catch
try {
await fetch('/app/admin/mcp-friends-and-family', { method: 'POST', body: JSON.stringify({ entries }) })
} catch (e) {
// e.message contains the specific invalid entry from the parser
} Prevention
- Strip 'Name <email>' formatting down to the bare email before posting.
- Validate each entry with an email regex client-side.
- Inspect the wrapped parser message for the offending entry.
When it happens
Trigger: POST /app/admin/mcp-friends-and-family with body.entries containing malformed emails, non-string entries, duplicates, or empty strings. The parser validates format before any DB lookup.
Common situations: Pasting a comma-separated blob that includes 'Name <email>' format; entries with spaces or semicolons; mixed types in the array (numbers, objects).
Related errors
- No tldraw account for: ${unknown.join(', ')}
- User not found ${q}
- objectId must be a 64-char lowercase hex string
- not a valid durable object id for the file namespace
- flag (string) is required
AI-assisted analysis of tldraw/tldraw@b31086b447 (2026-08-12).
Data as JSON: /api/errors/eb96749a594dea63.
Report an issue: GitHub.