{"record":{"id":"03d216eeb929a4e3","repo":"hcengineering/platform","slug":"unprocessable-entity","errorCode":null,"errorMessage":"Unprocessable Entity","messagePattern":"Unprocessable Entity","errorType":"http","errorClass":null,"httpStatus":422,"severity":"warning","filePath":"pods/link-preview/src/server.ts","lineNumber":101,"sourceCode":"  app.get(\n    '/details',\n    withAuthorization,\n    wrapRequest(ctx, 'getLinkPreviewDetails', async (ctx, req, res) => {\n      if (req.query === undefined) {\n        res.status(400).json({ message: 'Bad Request' })\n        return\n      }\n      if (req.query.q === undefined) {\n        res.status(400).json({ message: 'Bad Request' })\n        return\n      }\n      const url = req.query.q as string\n      try {\n        const result = await parseLinkPreviewDetails(ctx, config, url)\n        res.status(200).json(result)\n      } catch (err) {\n        console.error({ message: 'failed to parse link preview details', url, err })\n        res.status(422).json({ message: 'Unprocessable Entity' })\n      }\n    })\n  )\n\n  app.get('/api/v1/statistics', (req, res) => {\n    try {\n      const token = req.query.token as string\n      const payload = decodeToken(token)\n      const admin = payload.extra?.admin === 'true'\n      res.setHeader('Content-Type', 'application/json')\n      res.setHeader('Cache-Control', cacheControlNoCache)\n\n      const json = JSON.stringify({\n        metrics: metricsAggregate((ctx as any).metrics),\n        statistics: {\n          cpu: getCPUInfo(),\n          memory: getMemoryInfo()\n        },","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/pods/link-preview/src/server.ts#L83-L119","documentation":"The link-preview service's GET /api/v1/preview endpoint catches any exception thrown while parsing the URL given in the `q` query parameter and responds with HTTP 422 {\"message\":\"Unprocessable Entity\"}. The service throws this whenever parseLinkPreviewDetails fails — e.g. the target site is unreachable, blocks scraping, returns non-HTML, or the URL is malformed. The actual cause is only logged server-side, so the client only sees the generic 422 body.","triggerScenarios":"GET /api/v1/preview?q=<url> where the target URL cannot be fetched or parsed: invalid/unreachable URL, DNS failure, target returns 4xx/5xx, redirects to non-HTML content, TLS errors, or timeouts inside parseLinkPreviewDetails.","commonSituations":"Passing a URL without a scheme (example.com instead of https://example.com), previewing sites behind Cloudflare bot protection or a corporate firewall, previewing intranet URLs from a service with no network access, or previewing a page that was taken down.","solutions":["Verify the `q` parameter is a fully qualified, reachable absolute URL (curl it first) and retry.","Check the server logs for the 'failed to parse link preview details' entry containing the underlying error and the offending url.","If the target site blocks bots, configure the fetcher (user-agent, proxy) or skip previews for that domain.","Add client-side fallback UI rendering a plain link when a 422 is returned instead of surfacing an error."],"exampleFix":"// before\nawait fetch(`/api/v1/preview?q=${userUrl}`).then(r => r.json())\n// after\nconst res = await fetch(`/api/v1/preview?q=${encodeURIComponent(userUrl)}`)\nif (res.status === 422) {\n  renderPlainLink(userUrl)\n} else {\n  renderPreview(await res.json())\n}","handlingStrategy":"fallback","validationCode":"const u = new URL(q)\nif (!/^https?:$/.test(u.protocol)) throw new Error('Only http(s) URLs are supported')\nconst reachable = await fetch(u, { method: 'HEAD' }).then(r => r.ok).catch(() => false)\nif (!reachable) throw new Error('Target URL is not reachable')","typeGuard":"function isAbsoluteHttpUrl(value: unknown): value is string {\n  if (typeof value !== 'string') return false\n  try { const u = new URL(value); return u.protocol === 'http:' || u.protocol === 'https:' } catch { return false }\n}","tryCatchPattern":"try {\n  const preview = await getPreview(url)\n  renderPreview(preview)\n} catch (err) {\n  if (err.status === 422) renderPlainLink(url)\n  else throw err\n}","preventionTips":["Always send fully qualified absolute http(s) URLs, URL-encoded in the q parameter.","Pre-flight check reachability with a HEAD request before requesting a preview.","Design UI to gracefully fall back to a plain link when previews fail.","Maintain a skip-list for domains known to block scrapers."],"tags":["http","unprocessable-entity","url-fetch","link-preview"],"backgroundTag":"unprocessable-url-preview-failed","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}