From 203f7ac54012bef8a1b7f4db8b67dae095b249c9 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 14 Mar 2026 00:04:36 -0400 Subject: [PATCH] fix(Crowdin): Handle gzip CDN responses in fetchUrl Add transparent gzip decompression and improved response handling for CDN fetches. Introduces collectBody to aggregate response chunks, promisified zlib.gunzip (gunzip) and uses it to decompress responses with Content-Encoding: gzip. Also tightens redirect and HTTP error handling so callers always receive plain bytes. --- .github/scripts/sync-crowdin-distribution.js | 47 ++++++++++++++------ 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/.github/scripts/sync-crowdin-distribution.js b/.github/scripts/sync-crowdin-distribution.js index 9682427..0eef0f8 100644 --- a/.github/scripts/sync-crowdin-distribution.js +++ b/.github/scripts/sync-crowdin-distribution.js @@ -15,6 +15,10 @@ const https = require('node:https'); const fs = require('node:fs'); const path = require('node:path'); +const zlib = require('node:zlib'); +const { promisify } = require('node:util'); + +const gunzip = promisify(zlib.gunzip); const BASE_CDN = 'https://distributions.crowdin.net'; const OUTPUT_DIR = path.resolve(process.env.OUTPUT_DIR || 'dist-pages/crowdin-dist'); @@ -38,27 +42,44 @@ if (DISTRIBUTIONS.length === 0) { process.exit(1); } +/** + * Collects all data chunks from an HTTP response stream into a single Buffer. + * @param {import('http').IncomingMessage} res + * @returns {Promise} + */ +function collectBody(res) { + return new Promise((resolve, reject) => { + const chunks = []; + res.on('data', (chunk) => chunks.push(chunk)); + res.on('end', () => resolve(Buffer.concat(chunks))); + res.on('error', reject); + }); +} + /** * Fetches a URL, following redirects, and returns the body as a Buffer. + * Transparently decompresses gzip-encoded responses so callers always receive + * plain bytes (the Crowdin CDN stores content files with Content-Encoding: gzip, + * but jsDelivr re-serves the raw bytes without that header). * @param {string} url * @returns {Promise} */ -function fetchUrl(url) { +async function fetchUrl(url) { return new Promise((resolve, reject) => { - https.get(url, (res) => { - // Follow redirects + https.get(url, async (res) => { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { - return fetchUrl(res.headers.location).then(resolve).catch(reject); + return fetchUrl(res.headers.location).then(resolve, reject); + } + if (res.statusCode >= 400) { + return reject(new Error(`HTTP ${res.statusCode} for ${url}`)); + } + try { + const buffer = await collectBody(res); + const isGzip = res.headers['content-encoding'] === 'gzip'; + resolve(isGzip ? await gunzip(buffer) : buffer); + } catch (err) { + reject(err); } - const chunks = []; - res.on('data', (chunk) => chunks.push(chunk)); - res.on('end', () => { - if (res.statusCode >= 400) { - return reject(new Error(`HTTP ${res.statusCode} for ${url}`)); - } - resolve(Buffer.concat(chunks)); - }); - res.on('error', reject); }).on('error', reject); }); }