{"record":{"id":"f953be0a39906e23","repo":"vercel/ai","slug":"invalid-ui-open-link-url-params-url","errorCode":null,"errorMessage":"Invalid ui/open-link url: ${params.url}","messagePattern":"Invalid ui/open-link url: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"packages/react/src/mcp-apps/bridge.ts","lineNumber":94,"sourceCode":"    );\n  }\n  return { uri: params.uri };\n}\n\n/**\n * Validates `ui/open-link` params and allows only `https:`/`http:`/`mailto:`\n * URLs.\n */\nfunction assertOpenLinkParams(params: unknown): { url: string } {\n  if (!isJSONObject(params) || typeof params.url !== 'string') {\n    throw new Error('Invalid ui/open-link params');\n  }\n\n  let scheme: string;\n  try {\n    scheme = new URL(params.url).protocol;\n  } catch {\n    throw new Error(`Invalid ui/open-link url: ${params.url}`);\n  }\n\n  if (scheme !== 'https:' && scheme !== 'http:' && scheme !== 'mailto:') {\n    throw new Error(`Disallowed ui/open-link scheme: ${scheme}`);\n  }\n\n  return { url: params.url };\n}\n\n/**\n * Validates params for `ui/request-display-mode`.\n */\nfunction assertDisplayModeParams(params: unknown): {\n  mode: 'inline' | 'fullscreen' | 'pip';\n} {\n  if (\n    !isJSONObject(params) ||\n    (params.mode !== 'inline' &&","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/react/src/mcp-apps/bridge.ts#L76-L112","documentation":"The `url` string supplied by the iframe for `ui/open-link` could not be parsed by the `URL` constructor, meaning it is not an absolute, well-formed URL. The bridge rejects it before invoking the host's openLink handler, since relative or garbage strings cannot be safely opened.","triggerScenarios":"The iframe sends `ui/open-link` with a url like `\"example.com/page\"` (no scheme), `\"/relative/path\"`, an empty string, or a string containing spaces/invalid characters that fail `new URL()`.","commonSituations":"App constructs links from template strings with missing scheme; user-supplied text passed straight through as the url; backend returning relative paths that were never resolved to absolute URLs.","solutions":["Make the iframe send an absolute URL including scheme, e.g. `https://example.com/page`.","Resolve relative paths against `window.location.origin` before calling ui/open-link.","Trim/encode the url string and validate it with `new URL(url)` app-side first.","If links come from a backend, fix the backend to emit absolute URLs."],"exampleFix":"// before\nopenLink({ url: 'example.com/docs' })\n// after\nopenLink({ url: 'https://example.com/docs' })","handlingStrategy":"validation","validationCode":"// app-side, before requesting:\nlet parsed: URL;\ntry {\n  parsed = new URL(url); // throws on relative/malformed urls\n} catch {\n  parsed = new URL(url, window.location.origin); // resolve relative paths\n}","typeGuard":"function isAbsoluteUrl(value: string): boolean {\n  try { new URL(value); return true; } catch { return false; }\n}","tryCatchPattern":"try {\n  await openLink({ url });\n} catch (error) {\n  if (error instanceof Error && error.message.startsWith('Invalid ui/open-link url')) {\n    // retry with an absolutized, trimmed url\n    await openLink({ url: new URL(url.trim(), window.location.origin).href });\n  }\n}","preventionTips":["Always send absolute URLs with an explicit scheme.","Resolve relative paths against location.origin app-side.","Trim whitespace and encode the url before sending.","Sanitize user-supplied link text before using it as a url."],"tags":["mcp-apps","validation","url","iframe"],"backgroundTag":"malformed-url","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}