Skip to content

Commit a002994

Browse files
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.
1 parent 7b3f83e commit a002994

1 file changed

Lines changed: 34 additions & 13 deletions

File tree

.github/scripts/sync-crowdin-distribution.js

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
const https = require('node:https');
1616
const fs = require('node:fs');
1717
const path = require('node:path');
18+
const zlib = require('node:zlib');
19+
const { promisify } = require('node:util');
20+
21+
const gunzip = promisify(zlib.gunzip);
1822

1923
const BASE_CDN = 'https://distributions.crowdin.net';
2024
const OUTPUT_DIR = path.resolve(process.env.OUTPUT_DIR || 'dist-pages/crowdin-dist');
@@ -38,27 +42,44 @@ if (DISTRIBUTIONS.length === 0) {
3842
process.exit(1);
3943
}
4044

45+
/**
46+
* Collects all data chunks from an HTTP response stream into a single Buffer.
47+
* @param {import('http').IncomingMessage} res
48+
* @returns {Promise<Buffer>}
49+
*/
50+
function collectBody(res) {
51+
return new Promise((resolve, reject) => {
52+
const chunks = [];
53+
res.on('data', (chunk) => chunks.push(chunk));
54+
res.on('end', () => resolve(Buffer.concat(chunks)));
55+
res.on('error', reject);
56+
});
57+
}
58+
4159
/**
4260
* Fetches a URL, following redirects, and returns the body as a Buffer.
61+
* Transparently decompresses gzip-encoded responses so callers always receive
62+
* plain bytes (the Crowdin CDN stores content files with Content-Encoding: gzip,
63+
* but jsDelivr re-serves the raw bytes without that header).
4364
* @param {string} url
4465
* @returns {Promise<Buffer>}
4566
*/
46-
function fetchUrl(url) {
67+
async function fetchUrl(url) {
4768
return new Promise((resolve, reject) => {
48-
https.get(url, (res) => {
49-
// Follow redirects
69+
https.get(url, async (res) => {
5070
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
51-
return fetchUrl(res.headers.location).then(resolve).catch(reject);
71+
return fetchUrl(res.headers.location).then(resolve, reject);
72+
}
73+
if (res.statusCode >= 400) {
74+
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
75+
}
76+
try {
77+
const buffer = await collectBody(res);
78+
const isGzip = res.headers['content-encoding'] === 'gzip';
79+
resolve(isGzip ? await gunzip(buffer) : buffer);
80+
} catch (err) {
81+
reject(err);
5282
}
53-
const chunks = [];
54-
res.on('data', (chunk) => chunks.push(chunk));
55-
res.on('end', () => {
56-
if (res.statusCode >= 400) {
57-
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
58-
}
59-
resolve(Buffer.concat(chunks));
60-
});
61-
res.on('error', reject);
6283
}).on('error', reject);
6384
});
6485
}

0 commit comments

Comments
 (0)