From 3abf338767144ae3913a09af1434fe1ed60c6ff2 Mon Sep 17 00:00:00 2001 From: Ben Gauger Date: Fri, 8 May 2026 13:28:39 -0600 Subject: [PATCH] fix(Downloads): treat missing Content-Type as octet-stream (#848) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit download.kiwix.org (and some of its mirrors) don't always set a Content-Type header on .zim responses. The MIME validator was reading `headers['content-type'] || ''`, then running each allowlist entry through `''.includes(...)` which is always false, so every download from those hosts was rejected with `MIME type is not allowed`. RFC 7231 §3.1.1.5 says missing Content-Type may be treated as application/octet-stream by the recipient, and that's already in every binary-content allowlist we use (ZIM, PMTILES, base assets). Default the missing case to that and the validator does the right thing. Strict callers that don't list octet-stream still reject as before. --- admin/app/utils/downloads.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/admin/app/utils/downloads.ts b/admin/app/utils/downloads.ts index 5d91d61..23988da 100644 --- a/admin/app/utils/downloads.ts +++ b/admin/app/utils/downloads.ts @@ -47,7 +47,14 @@ export async function doResumableDownload({ timeout, }) - const contentType = headResponse.headers['content-type'] || '' + // Some upstream hosts (notably download.kiwix.org for .zim files) don't set a + // Content-Type header at all. Per RFC 7231 §3.1.1.5, "if no Content-Type is + // provided" the recipient may treat it as application/octet-stream — which is + // already in every binary-content allowlist we use (ZIM, PMTILES, base assets). + // Without this default, the validator below throws `MIME type is not allowed` + // and breaks all downloads from kiwix's primary host (#848). + const contentType = + headResponse.headers['content-type'] || 'application/octet-stream' const totalBytes = parseInt(headResponse.headers['content-length'] || '0') const supportsRangeRequests = headResponse.headers['accept-ranges'] === 'bytes'