toeverything/AFFiNE · error

Link preview unavailable

Error message

Link preview unavailable

What it means

readLinkPreviewResponse throws 'Link preview unavailable' when the fetch Response for a link preview is not ok (non-2xx HTTP status) or has no readable body stream. The library requires a successful HTTP response with a body before it will attempt to stream and parse preview JSON. This is a guard against processing error responses or empty bodies from the preview backend.

Solutions

  1. Check response.ok and response.status before calling readLinkPreviewResponse, and handle non-2xx statuses explicitly
  2. Verify the fetch was not made with mode:'no-cors' (opaque responses have a null body)
  3. Inspect server logs / backend health for the preview endpoint returning non-2xx
  4. Wrap the call in try-catch and degrade gracefully (skip preview) when unavailable

Example fix

// before
const res = await fetch(previewUrl);
const data = await readLinkPreviewResponse(res);
// after
const res = await fetch(previewUrl);
if (!res.ok) {
  console.warn(`Preview fetch failed: ${res.status}`);
} else {
  const data = await readLinkPreviewResponse(res);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// before calling
if (!response.ok || !response.body) {
  throw new Error(`Preview unavailable: HTTP ${response.status}`);
}

Type guard

function hasReadableBody(res: Response): res is Response & { body: ReadableStream } {
  return res.ok && res.body !== null;
}

Try / catch

try {
  const data = await readLinkPreviewResponse(response);
} catch (e) {
  if (e.message === 'Link preview unavailable') {
    // degrade: render plain link instead of preview card
  } else throw e;
}

Prevention

When it happens

Trigger: Calling readLinkPreviewResponse (via the link-preview data()/readShareLinkPreview path) with a Response whose response.ok is false (404, 500, 403, etc.) or whose response.body is null (e.g. opaque/no-cors fetch responses, 204 No Content, or body already consumed).

Common situations: The preview backend is down or returns 5xx; the shared link was deleted (404); a fetch made with mode:'no-cors' yields an opaque response with null body; the network proxy returns an error page; the response was consumed elsewhere before being passed in.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of toeverything/AFFiNE@2af30773ae (2026-09-15). Data as JSON: /api/errors/c266feb99a74a848. Report an issue: GitHub.

Appendix: source

Thrown at blocksuite/affine/shared/src/services/link-preview-service/response.ts:91

    .optional()
    .catch(undefined),
});

export type LinkPreviewResponseData = z.infer<typeof LinkPreviewResponseSchema>;

export function parseLinkPreviewResponse(
  value: unknown
): LinkPreviewResponseData | undefined {
  const result = LinkPreviewResponseSchema.safeParse(value);
  return result.success ? result.data : undefined;
}

export async function readLinkPreviewResponse(
  response: Response,
  maxBytes = 4 * 1024 * 1024
) {
  if (!response.ok || !response.body)
    throw new Error('Link preview unavailable');
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let bytes = 0;
  let json = '';
  try {
    for (;;) {
      const { done, value } = await reader.read();
      if (done) break;
      bytes += value.byteLength;
      if (bytes > maxBytes) {
        await reader.cancel();
        throw new Error('Link preview response too large');
      }
      json += decoder.decode(value, { stream: true });
    }
    json += decoder.decode();
  } finally {
    reader.releaseLock();

View on GitHub (pinned to 2af30773ae)