{"record":{"id":"301fde105afdcbcf","repo":"tinyhumansai/openhuman","slug":"discord-link-start-response-missing-required-strin","errorCode":null,"errorMessage":"Discord link start response missing required string field: linkToken","messagePattern":"Discord link start response missing required string field: linkToken","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/src/services/api/channelConnectionsApi.ts","lineNumber":79,"sourceCode":"  if (!Array.isArray(unwrapped)) {\n    throw new Error(`${context} returned an invalid response shape`);\n  }\n  return unwrapped as T[];\n}\n\nfunction expectObject<T extends object>(payload: unknown, context: string): T {\n  const unwrapped = unwrapCliEnvelope<unknown>(payload);\n  const record = asRecord(unwrapped);\n  if (!record) {\n    throw new Error(`${context} returned an invalid response shape`);\n  }\n  return record as T;\n}\n\nfunction expectDiscordLinkStart(payload: unknown): DiscordLinkStartResult {\n  const record = expectObject<Record<string, unknown>>(payload, 'Discord link start');\n  if (typeof record.linkToken !== 'string' || !record.linkToken) {\n    throw new Error('Discord link start response missing required string field: linkToken');\n  }\n  if (typeof record.instructions !== 'string') {\n    throw new Error('Discord link start response missing required string field: instructions');\n  }\n  return { linkToken: record.linkToken, instructions: record.instructions };\n}\n\nfunction expectDiscordLinkComplete(payload: unknown): DiscordLinkCheckResult {\n  const record = expectObject<Record<string, unknown>>(payload, 'Discord link complete');\n  if (typeof record.linked !== 'boolean') {\n    throw new Error('Discord link complete response missing required boolean field: linked');\n  }\n  const details =\n    record.details !== undefined && record.details !== null\n      ? (record.details as Record<string, unknown>)\n      : null;\n  return { linked: record.linked, details };\n}","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/app/src/services/api/channelConnectionsApi.ts#L61-L97","documentation":"Thrown by expectDiscordLinkStart() while validating the payload of the Discord link-start RPC: the unwrapped response object does not contain a non-empty string `linkToken`. The linkToken is the one-time token the user DMs to the Discord bot to pair their account; without it the pairing flow cannot proceed. This is a response-shape contract break between the UI's validator and whatever the core returned.","triggerScenarios":"The core's Discord link.start handler returned an error object or partial result that still unwrapped to a record (e.g. {ok:false}); version skew where an older core's response uses a different field name (link_token snake_case not mapped); an envelope-unwrapping step (unwrapCliEnvelope) stripped the wrong layer and left an empty object.","commonSituations":"UI newer than the running core after a partial update or with OPENHUMAN_CORE_REUSE_EXISTING=1; a core handler change renamed fields without a frontend counterpart; the Discord provider not configured server-side so start degrades to a stub response.","solutions":["Restart/update the core so the Discord connections controller matches the UI's expected schema (linkToken + instructions).","Inspect the raw RPC payload: log the response before expectDiscordLinkStart to see which field name/shape the core actually sent.","Check that the Discord channel provider is configured in the core (bot token present), since a missing provider can yield a stubbed start response.","If the core legitimately returns snake_case, normalize in expectObject's unwrap step instead of failing."],"exampleFix":"// before\nif (typeof record.linkToken !== 'string' || !record.linkToken) {\n  throw new Error('Discord link start response missing required string field: linkToken');\n}\n\n// after — tolerate snake_case from older cores, fail loudly otherwise\nconst linkToken = typeof record.linkToken === 'string' && record.linkToken\n  ? record.linkToken\n  : typeof record.link_token === 'string' ? record.link_token : null;\nif (!linkToken) {\n  throw new Error('Discord link start response missing required string field: linkToken');\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function isDiscordLinkStartPayload(v: unknown): v is { linkToken: string; instructions: string } {\n  const r = v as Record<string, unknown> | null;\n  return Boolean(r)\n    && typeof r!.linkToken === 'string' && r!.linkToken.length > 0\n    && typeof r!.instructions === 'string';\n}","tryCatchPattern":"try {\n  const start = await startDiscordLink();\n} catch (e) {\n  if (e instanceof Error && e.message.includes('missing required string field: linkToken')) {\n    notify('Pairing unavailable — restart Core and retry.');\n  } else throw e;\n}","preventionTips":["Keep core and UI versions aligned after updates.","Log the raw RPC payload in dev when a schema validator fails.","Reuse the shipped expect* validators for mocks so tests match production shapes."],"tags":["discord","channel-linking","schema-validation","core-rpc","version-skew"],"backgroundTag":"schema-validation-failed","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}