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
- Check response.ok and response.status before calling readLinkPreviewResponse, and handle non-2xx statuses explicitly
- Verify the fetch was not made with mode:'no-cors' (opaque responses have a null body)
- Inspect server logs / backend health for the preview endpoint returning non-2xx
- 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
- Always check response.ok and response.status before consuming a preview response
- Avoid fetch mode:'no-cors' for preview requests (opaque responses have null body)
- Do not consume response.body before passing the Response to readLinkPreviewResponse
- Monitor the preview endpoint's availability and status codes in production
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)