All files / node getHeader.ts

79.62% Statements 43/54
55.55% Branches 10/18
100% Functions 2/2
79.62% Lines 43/54

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 901x 1x                                         3x 3x 3x 3x 3x 3x 45x 45x   3x 3x 3x 3x 3x 3x 3x   3x 3x 3x 3x                     2x 2x 2x   2x 2x   2x 2x 1x 1x 1x 1x 1x     1x   2x 2x 2x 2x 2x 2x 2x 2x                   2x  
import * as http from 'node:http';
import * as https from 'node:https';
 
/**
 * Result information from getHeader.
 * @public
 */
export interface HeaderResult {
	ok: boolean;
	status?: number;
	size?: number;
	magic: boolean | null;
	headers?: Record<string, string>;
	error?: Error;
}
 
interface RequestResult {
	status: number;
	headers: Record<string, string>;
	buffer?: Buffer;
}
 
async function nodeRequest(u: URL, method: string, headers?: Record<string, string>): Promise<RequestResult> {
	return new Promise((resolve, reject) => {
		const reqFn = u.protocol === 'https:' ? https.request : http.request;
		const req = reqFn(u, { method, headers }, (res) => {
			const headersObj: Record<string, string> = {};
			for (const [k, v] of Object.entries(res.headers)) {
				headersObj[k] = Array.isArray(v) ? v.join(',') : (v ?? '');
			}
 
			const chunks: Buffer[] = [];
			res.on('data', (c) => chunks.push(Buffer.from(c)));
			res.on('end', () => {
				const buffer = chunks.length ? Buffer.concat(chunks) : undefined;
				resolve({ status: res.statusCode ?? 0, headers: headersObj, buffer });
			});
		});
 
		req.on('error', (err) => reject(err));
		req.end();
	});
}
 
/**
 * Perform an HTTP HEAD request to retrieve the file size and verify existence;
 * when `check` is true, fetch a small range and inspect the magic number to confirm the URL points to a valid PDF.
 * If the server does not support range requests, `isPdf` will be set to `false`.
 * @param url - The URL of the PDF file to check. Can be a string or URL object.
 * @param check - When `true`, download a small byte range (first 4 bytes) to validate the file signature by checking for '%PDF' magic bytes. Default: `false`.
 * @returns - A Promise that resolves to a HeaderResult object containing the response status, size, headers, and PDF validation result.
 * @public
 */
export async function getHeader(url: string | URL, check: boolean = false): Promise<HeaderResult> {
	try {
		const u = typeof url === 'string' ? new URL(url) : url;
 
		const headResp = await nodeRequest(u, 'HEAD');
		const size = headResp.headers['content-length'] ? parseInt(headResp.headers['content-length'], 10) : undefined;
 
		let magic: boolean | null = null;
		if (check) {
			const rangeResp = await nodeRequest(u, 'GET', { Range: 'bytes=0-4' });
			if (rangeResp.status >= 200 && rangeResp.status < 300 && rangeResp.buffer) {
				const headerStr = rangeResp.buffer.slice(0, 4).toString('utf8');
				magic = headerStr.startsWith('%PDF');
			} else {
				magic = false;
			}
		}
 
		return {
			ok: headResp.status >= 200 && headResp.status < 300,
			status: headResp.status,
			size,
			magic,
			headers: headResp.headers,
		};
	} catch (error) {
		return {
			ok: false,
			status: undefined,
			size: undefined,
			magic: false,
			headers: {},
			error: new Error(String(error)),
		};
	}
}