From eef5b6cd3788415a6de73a1208863c00c4b7952e Mon Sep 17 00:00:00 2001 From: Chris Sherwood Date: Fri, 25 Sep 2026 13:15:35 -0700 Subject: [PATCH] fix(translate): forward upstream redirects instead of following them urlopen() follows redirects silently. Kiwix answers a book root such as /content/ with a 302 to the ZIM's main page, so the proxy served that page's HTML at the original, shallower URL. Every relative link and stylesheet then resolved one directory too high: the page lost its styling and every article link 404'd (e.g. atenolol/ -> /content/atenolol/). Kiwix's /random is a 302 too and broke the same way. Use an opener that returns 3xx to the browser like any other non-200 response, and keep an absolute Location on this origin rather than the upstream container name. Bumps the catalog image to 0.1.1. Co-Authored-By: Claude Opus 5.5 (1M context) --- admin/database/seeders/service_seeder.ts | 2 +- install/nomad-translate/proxy.py | 26 +++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/admin/database/seeders/service_seeder.ts b/admin/database/seeders/service_seeder.ts index 20b40166..4abd4fb7 100644 --- a/admin/database/seeders/service_seeder.ts +++ b/admin/database/seeders/service_seeder.ts @@ -550,7 +550,7 @@ export default class ServiceSeeder extends BaseSeeder { description: 'Read the Information Library in another language. Machine translation that works offline, on CPU', icon: 'IconWorld', - container_image: 'ghcr.io/crosstalk-solutions/project-nomad-translate:0.1.0', + container_image: 'ghcr.io/crosstalk-solutions/project-nomad-translate:0.1.1', source_repo: 'https://github.com/browsermt/bergamot-translator', container_command: null, container_config: JSON.stringify({ diff --git a/install/nomad-translate/proxy.py b/install/nomad-translate/proxy.py index 78ba1aea..553f164b 100644 --- a/install/nomad-translate/proxy.py +++ b/install/nomad-translate/proxy.py @@ -40,6 +40,24 @@ MAX_TRANSLATE_BYTES = int(os.environ.get("MAX_TRANSLATE_BYTES", str(4 * 1024 * 1 COOKIE = "nomadlang" + +class _NoRedirect(urllib.request.HTTPRedirectHandler): + """Hand upstream redirects back to the browser instead of following them. + + Kiwix answers a book root such as /content/ with a 302 to the ZIM's + main page. Following it here would serve that page's HTML at the ORIGINAL + URL, so every relative link and stylesheet in it resolves one directory + too high: styling is lost and every article link 404s. + """ + + def redirect_request(self, *args, **kwargs): + return None + + +# urlopen() follows redirects by default; this opener raises them as HTTPError +# so they are forwarded below like any other non-200 response. +_opener = urllib.request.build_opener(_NoRedirect) + # Display names for the languages Bergamot's tiny model set covers. A language # only appears in the control if its model is actually on disk. LANG_NAMES = { @@ -226,12 +244,18 @@ class Handler(BaseHTTPRequestHandler): # HTML can be rewritten without decompressing first. try: - response = urllib.request.urlopen(request, timeout=120) + response = _opener.open(request, timeout=120) status, headers, data = response.status, response.headers, response.read() except urllib.error.HTTPError as exc: # Forwarded as-is, which is how Kiwix's own 404 and 400 pages keep # coming back byte for byte. status, headers, data = exc.code, exc.headers, exc.read() + # An absolute Location would point the browser at the upstream + # container name, which it cannot reach. Keep it on this origin. + location = headers.get("Location", "") + if location.startswith(UPSTREAM): + del headers["Location"] + headers["Location"] = location[len(UPSTREAM):] or "/" except Exception as exc: message = f"Translation proxy could not reach the library: {exc}".encode() self.send_response(502)