hcengineering/platform · warning

Unprocessable Entity

Error message

Unprocessable Entity

What it means

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.

Source

Thrown at pods/link-preview/src/server.ts:101

  app.get(
    '/details',
    withAuthorization,
    wrapRequest(ctx, 'getLinkPreviewDetails', async (ctx, req, res) => {
      if (req.query === undefined) {
        res.status(400).json({ message: 'Bad Request' })
        return
      }
      if (req.query.q === undefined) {
        res.status(400).json({ message: 'Bad Request' })
        return
      }
      const url = req.query.q as string
      try {
        const result = await parseLinkPreviewDetails(ctx, config, url)
        res.status(200).json(result)
      } catch (err) {
        console.error({ message: 'failed to parse link preview details', url, err })
        res.status(422).json({ message: 'Unprocessable Entity' })
      }
    })
  )

  app.get('/api/v1/statistics', (req, res) => {
    try {
      const token = req.query.token as string
      const payload = decodeToken(token)
      const admin = payload.extra?.admin === 'true'
      res.setHeader('Content-Type', 'application/json')
      res.setHeader('Cache-Control', cacheControlNoCache)

      const json = JSON.stringify({
        metrics: metricsAggregate((ctx as any).metrics),
        statistics: {
          cpu: getCPUInfo(),
          memory: getMemoryInfo()
        },

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Verify the `q` parameter is a fully qualified, reachable absolute URL (curl it first) and retry.
  2. Check the server logs for the 'failed to parse link preview details' entry containing the underlying error and the offending url.
  3. If the target site blocks bots, configure the fetcher (user-agent, proxy) or skip previews for that domain.
  4. Add client-side fallback UI rendering a plain link when a 422 is returned instead of surfacing an error.

Example fix

// before
await fetch(`/api/v1/preview?q=${userUrl}`).then(r => r.json())
// after
const res = await fetch(`/api/v1/preview?q=${encodeURIComponent(userUrl)}`)
if (res.status === 422) {
  renderPlainLink(userUrl)
} else {
  renderPreview(await res.json())
}
Defensive patterns

Strategy: fallback

Validate before calling

const u = new URL(q)
if (!/^https?:$/.test(u.protocol)) throw new Error('Only http(s) URLs are supported')
const reachable = await fetch(u, { method: 'HEAD' }).then(r => r.ok).catch(() => false)
if (!reachable) throw new Error('Target URL is not reachable')

Type guard

function isAbsoluteHttpUrl(value: unknown): value is string {
  if (typeof value !== 'string') return false
  try { const u = new URL(value); return u.protocol === 'http:' || u.protocol === 'https:' } catch { return false }
}

Try / catch

try {
  const preview = await getPreview(url)
  renderPreview(preview)
} catch (err) {
  if (err.status === 422) renderPlainLink(url)
  else throw err
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29). Data as JSON: /api/errors/03d216eeb929a4e3. Report an issue: GitHub.