/** * HTTP encoding utilities for Accept-Encoding parsing and compression algorithm selection */ // ============================================================================= // Types // ============================================================================= /** * Supported compression algorithms */ export type TCompressionAlgorithm = 'br' | 'gzip' | 'deflate' | 'identity'; /** * Parsed encoding preference with quality value */ export interface IEncodingPreference { encoding: string; q: number; } // ============================================================================= // Accept-Encoding Parsing // ============================================================================= /** * Parse Accept-Encoding header with quality values * * @example * parseAcceptEncoding('gzip, deflate, br;q=1.0, identity;q=0.5') * // Returns: [{ encoding: 'br', q: 1.0 }, { encoding: 'gzip', q: 1.0 }, { encoding: 'deflate', q: 1.0 }, { encoding: 'identity', q: 0.5 }] */ export function parseAcceptEncoding(header: string | null): IEncodingPreference[] { if (!header) { return [{ encoding: 'identity', q: 1.0 }]; } const preferences: IEncodingPreference[] = []; for (const part of header.split(',')) { const trimmed = part.trim(); if (!trimmed) continue; const [encoding, ...params] = trimmed.split(';'); let q = 1.0; for (const param of params) { const [key, value] = param.trim().split('='); if (key?.toLowerCase() === 'q' && value) { q = parseFloat(value) || 0; } } if (q > 0) { preferences.push({ encoding: encoding.trim().toLowerCase(), q }); } } // Sort by quality (descending) return preferences.sort((a, b) => b.q - a.q); } /** * Select best encoding from client preferences and server support */ export function selectEncoding( acceptEncoding: string | null, supported: TCompressionAlgorithm[] ): TCompressionAlgorithm { const preferences = parseAcceptEncoding(acceptEncoding); for (const pref of preferences) { if (pref.encoding === '*') { // Wildcard - use first supported (brotli preferred) return supported[0] ?? 'identity'; } if (supported.includes(pref.encoding as TCompressionAlgorithm)) { return pref.encoding as TCompressionAlgorithm; } } return 'identity'; } // ============================================================================= // Compressibility Check // ============================================================================= /** * Default MIME type patterns that should be compressed */ const DEFAULT_COMPRESSIBLE_TYPES = [ 'text/', 'application/json', 'application/javascript', 'application/xml', 'application/xhtml+xml', 'application/rss+xml', 'application/atom+xml', 'application/x-javascript', 'application/ld+json', 'application/manifest+json', 'application/vnd.api+json', 'image/svg+xml', 'image/x-icon', 'font/ttf', 'font/otf', ]; /** * MIME types that should never be compressed (already compressed) */ const NEVER_COMPRESS = [ 'image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/avif', 'video/', 'audio/', 'application/zip', 'application/gzip', 'application/x-gzip', 'application/x-bzip2', 'application/x-7z-compressed', 'application/x-rar-compressed', 'application/wasm', 'font/woff', 'font/woff2', ]; /** * Check if content type should be compressed */ export function isCompressible( contentType: string | null, customTypes?: string[] ): boolean { if (!contentType) return false; const lowerType = contentType.toLowerCase().split(';')[0].trim(); // Never compress already-compressed formats for (const pattern of NEVER_COMPRESS) { if (pattern.endsWith('/')) { if (lowerType.startsWith(pattern)) return false; } else { if (lowerType === pattern) return false; } } const types = customTypes ?? DEFAULT_COMPRESSIBLE_TYPES; return types.some(pattern => pattern.endsWith('/') ? lowerType.startsWith(pattern) : lowerType.includes(pattern) ); } /** * Get default compressible types (for configuration reference) */ export function getDefaultCompressibleTypes(): string[] { return [...DEFAULT_COMPRESSIBLE_TYPES]; }