{"record":{"id":"c97a677a153aff6b","repo":"hcengineering/platform","slug":"invalid-url","errorCode":"INVALID_URL","errorMessage":"Invalid URL: ${urlString}","messagePattern":"Invalid URL: (.+?)","errorType":"error_code","errorClass":"LinkPreviewError","httpStatus":null,"severity":"error","filePath":"pods/link-preview/src/parse.ts","lineNumber":211,"sourceCode":"  if (ipType === 6) return isBlockedIpv6(host)\n\n  // Some Node versions are stricter about IPv6 parsing. If it still looks like an IPv6 literal,\n  // apply our IPv6 checks anyway (covers IPv6-mapped IPv4 forms like ::ffff:7f00:1).\n  if (host.includes(':') && isBlockedIpv6(host)) return true\n\n  // Hostname is not an IP literal. Keep legacy explicit localhost-ish blocks.\n  // (We intentionally do not attempt DNS resolution here.)\n  if (host.endsWith('.localhost')) return true\n\n  return false\n}\n\nfunction validateUrl (urlString: string): URL {\n  let url: URL\n  try {\n    url = new URL(urlString)\n  } catch {\n    throw new LinkPreviewError(`Invalid URL: ${urlString}`, 'INVALID_URL')\n  }\n\n  // Only allow HTTP(S) protocols\n  if (!['http:', 'https:'].includes(url.protocol)) {\n    throw new LinkPreviewError(\n      `Invalid protocol: ${url.protocol}. Only HTTP and HTTPS are allowed.`,\n      'INVALID_PROTOCOL'\n    )\n  }\n\n  // SSRF protection: block private/internal hosts and IP literals (incl. IPv6-mapped IPv4)\n  if (isBlockedHost(url.hostname)) {\n    throw new LinkPreviewError('Blocked URL: Access to internal addresses is not allowed.', 'BLOCKED_URL')\n  }\n\n  return url\n}\n","sourceCodeStart":193,"sourceCodeEnd":229,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/pods/link-preview/src/parse.ts#L193-L229","documentation":"validateUrl parses a URL string with `new URL()`. Strings that are not valid absolute URLs (including relative paths or malformed input) make the URL constructor throw, which is converted into LinkPreviewError with code INVALID_URL.","triggerScenarios":"fetchOEmbedData / fetchWithValidatedRedirects / loadImageSize / parsedUrl called with '', 'not-a-url', 'example.com' (no scheme), or an unencoded/fragmented malformed string.","commonSituations":"User-typed input in a link field without normalization; missing protocol when pasting hostnames; stored bookmarks from an older schema; empty url query parameter.","solutions":["Normalize input by prepending https:// when the scheme is missing before calling the API","Validate the URL client-side with `new URL()` and reject failures early","Trim whitespace and encode the URL in the query string"],"exampleFix":"// before\nvalidateUrl('example.com/page') // throws\n// after\nconst raw = 'example.com/page'\nvalidateUrl(raw.startsWith('http') ? raw : 'https://' + raw)","handlingStrategy":"validation","validationCode":"function isAbsoluteHttpUrl(s: string): boolean {\n  try { const u = new URL(s.trim()); return Boolean(u.hostname) } catch { return false }\n}\nif (!isAbsoluteHttpUrl(input)) throw new Error('provide an absolute http(s) URL')","typeGuard":null,"tryCatchPattern":"try {\n  return await fetchOEmbedData(client, url)\n} catch (err) {\n  if (err instanceof LinkPreviewError && err.code === 'INVALID_URL') {\n    return await fetchOEmbedData(client, 'https://' + url)\n  }\n  throw err\n}","preventionTips":["Trim and normalize user input (add https:// if scheme missing)","Use `new URL()` as a client-side pre-check before sending","Encode the URL parameter in the query string","Reject empty/relative strings at the form level"],"tags":["url","validation","input-error"],"backgroundTag":"invalid-url","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}