feat: añadir soporte para compresión Brotli y mejorar la selección de codificación

This commit is contained in:
2025-10-08 13:04:03 -05:00
parent 96f7067193
commit 67b187c3c0

View File

@@ -2,7 +2,11 @@ import { createServer, IncomingMessage, ServerResponse } from "node:http";
import { promises as fs } from "node:fs"; import { promises as fs } from "node:fs";
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import { gzipSync, brotliCompressSync } from "node:zlib"; import {
gzipSync,
brotliCompressSync,
constants as zlibConstants,
} from "node:zlib";
import path from "node:path"; import path from "node:path";
import ejs from "ejs"; import ejs from "ejs";
@@ -168,6 +172,49 @@ function acceptsEncoding(req: IncomingMessage, enc: string): boolean {
.includes(enc); .includes(enc);
} }
// Parse Accept-Encoding with q-values and return a map of encoding -> q
function parseAcceptEncoding(req: IncomingMessage): Map<string, number> {
const header = (req.headers["accept-encoding"] as string) || "";
const map = new Map<string, number>();
if (!header) {
// If header missing, identity is acceptable
map.set("identity", 1);
return map;
}
const parts = header.split(",");
for (const raw of parts) {
const part = raw.trim();
if (!part) continue;
const [name, ...params] = part.split(";");
let q = 1;
for (const p of params) {
const [k, v] = p.split("=").map((s) => s.trim());
if (k === "q" && v) {
const n = Number(v);
if (!Number.isNaN(n)) q = n;
}
}
map.set(name.toLowerCase(), q);
}
// Ensure identity exists unless explicitly disabled (q=0)
if (!map.has("identity")) map.set("identity", 1);
return map;
}
// Choose the best compression given the request and mime type.
function pickEncoding(
req: IncomingMessage,
mime: string
): "br" | "gzip" | "identity" {
if (!shouldCompress(mime)) return "identity";
const encs = parseAcceptEncoding(req);
const qBr = encs.get("br") ?? 0;
const qGzip = encs.get("gzip") ?? 0;
if (qBr > 0) return "br";
if (qGzip > 0) return "gzip";
return "identity";
}
function shouldCompress(mime: string): boolean { function shouldCompress(mime: string): boolean {
return ( return (
mime.startsWith("text/") || mime.startsWith("text/") ||
@@ -231,14 +278,25 @@ const sendResponse = async (
...(stat ? { "Last-Modified": stat.mtime.toUTCString() } : {}), ...(stat ? { "Last-Modified": stat.mtime.toUTCString() } : {}),
}; };
if (shouldCompress(mimeType) && acceptsEncoding(req, "gzip")) { // Prefer Brotli over Gzip when supported
try { const chosen = pickEncoding(req, mimeType);
try {
if (chosen === "br") {
body = brotliCompressSync(data, {
params: {
[zlibConstants.BROTLI_PARAM_QUALITY]: 4, // fast, good ratio for text
[zlibConstants.BROTLI_PARAM_MODE]: zlibConstants.BROTLI_MODE_TEXT,
},
});
headers["Content-Encoding"] = "br";
headers["Vary"] = "Accept-Encoding";
} else if (chosen === "gzip") {
body = gzipSync(data); body = gzipSync(data);
headers["Content-Encoding"] = "gzip"; headers["Content-Encoding"] = "gzip";
headers["Vary"] = "Accept-Encoding"; headers["Vary"] = "Accept-Encoding";
} catch {
// Si falla compresión, enviar sin comprimir
} }
} catch {
// Si falla compresión, enviar sin comprimir
} }
res.writeHead(statusCode, applySecurityHeadersForRequest(req, headers)); res.writeHead(statusCode, applySecurityHeadersForRequest(req, headers));
@@ -293,14 +351,24 @@ const renderTemplate = async (
ETag: etag, ETag: etag,
}; };
if (acceptsEncoding(req, "gzip")) { const chosenDyn = pickEncoding(req, "text/html; charset=utf-8");
try { try {
if (chosenDyn === "br") {
respBody = brotliCompressSync(htmlBuffer, {
params: {
[zlibConstants.BROTLI_PARAM_QUALITY]: 4,
[zlibConstants.BROTLI_PARAM_MODE]: zlibConstants.BROTLI_MODE_TEXT,
},
});
headers["Content-Encoding"] = "br";
headers["Vary"] = "Accept-Encoding";
} else if (chosenDyn === "gzip") {
respBody = gzipSync(htmlBuffer); respBody = gzipSync(htmlBuffer);
headers["Content-Encoding"] = "gzip"; headers["Content-Encoding"] = "gzip";
headers["Vary"] = "Accept-Encoding"; headers["Vary"] = "Accept-Encoding";
} catch {
// continuar sin comprimir
} }
} catch {
// continuar sin comprimir
} }
res.writeHead(statusCode, applySecurityHeadersForRequest(req, headers)); res.writeHead(statusCode, applySecurityHeadersForRequest(req, headers));