A download interface can show Unexpected token '<' even when the user's post link is valid. The metadata endpoint may have returned an HTML login page, an upstream error page, or a proxy response instead of JSON. Validate the HTTP response contract before parsing its body so users get an actionable failure and developers keep the actual cause.
I maintain Twitter Viewer and its twitter video downloader. This article uses a fictional metadata endpoint and a small executable response parser. It describes an interface boundary, not Twitter Viewer's private implementation.
Check status before interpreting a successful result
fetch resolves for HTTP errors such as 404 and 500. A resolved promise only means a response arrived. Code that immediately calls response.json() can hide an upstream 502 behind a JSON syntax error if the gateway generated an HTML page.
Give each kind of failure a stable code. A non-success status is different from a successful response with an unexpected media type, and both differ from a JSON object with missing fields.
export async function readMetadata(response) {
const fail = (code, message) => {
throw Object.assign(new Error(message), { code });
};
if (!response.ok) fail('HTTP', `Metadata request returned ${response.status}`);
const type = (response.headers.get('content-type') || '')
.split(';', 1)[0].trim().toLowerCase();
if (type !== 'application/json') {
fail('TYPE', 'Metadata endpoint did not return application/json');
}
let value;
try {
value = await response.json();
} catch {
fail('JSON', 'Metadata endpoint returned invalid JSON');
}
if (!value || typeof value !== 'object' || Array.isArray(value) ||
typeof value.postId !== 'string' || !Array.isArray(value.variants)) {
fail('SCHEMA', 'Metadata response is missing postId or variants');
}
return value;
}
Save this as metadata.mjs. The schema check is intentionally small: it establishes that the outer object can be consumed by a caller. A real application should validate each variant's properties before rendering a link or initiating a transfer. A string with the right property name is not automatically a trusted fetch destination.
This contract accepts application/json with optional parameters, including application/json; charset=utf-8. It intentionally rejects vendor media types. If your API uses a vendor JSON type, add it explicitly to the contract and its tests instead of accepting any header that happens to contain the substring json.
Exercise failures without making a network request
Modern Node.js includes the Response class. A test can construct the exact bad response a proxy might return:
import test from 'node:test';
import assert from 'node:assert/strict';
import { readMetadata } from './metadata.mjs';
const json = text => new Response(text, {
headers: { 'content-type': 'application/json; charset=utf-8' }
});
test('accepts the expected envelope', async () => {
assert.deepEqual(await readMetadata(json('{"postId":"123","variants":[]}')),
{ postId: '123', variants: [] });
});
test('classifies status, content, and shape separately', async () => {
await assert.rejects(readMetadata(new Response('<h1>Bad gateway</h1>',
{ status: 502 })), { code: 'HTTP' });
await assert.rejects(readMetadata(new Response('<html>Sign in</html>',
{ headers: { 'content-type': 'text/html' } })), { code: 'TYPE' });
await assert.rejects(readMetadata(json('{')), { code: 'JSON' });
await assert.rejects(readMetadata(json('{"variants":[]}')), { code: 'SCHEMA' });
});
Save that as metadata.test.mjs and run node --test metadata.test.mjs. No API credentials, social account, or live video are needed. These are response-contract tests, so a platform outage cannot make the test results ambiguous.
Keep diagnostics out of the visible error
For the user, an HTTP failure might warrant “Video lookup is temporarily unavailable; retry shortly.” An HTML response might warrant the same recovery advice while retaining a different internal code. Log the status, response type, request ID, and endpoint name where appropriate. Avoid pasting an entire unexpected HTML body into an error toast: it may contain account details, tokens, or internal infrastructure text.
Once metadata is valid, downloading remains a separate operation with its own response contract. Receiving a list of variants does not prove a file reached the user's device. Native public videos, externally embedded players, and inaccessible posts also require distinct source handling; a generic JSON parsing message should never be the explanation for all of them.
Top comments (0)