From 2b49eda7cb1a83842639d46f921d693897061d5d Mon Sep 17 00:00:00 2001 From: Roman Priesol Date: Wed, 9 Sep 2026 15:55:48 +0200 Subject: [PATCH] Fix kiwix proxy 502ing on article links with non-ASCII titles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sinatra decodes a route's splat before handing it to the block, so an article link like /kiwix/content/wikipedia/Dlhé_diely arrived as raw UTF-8, not percent-encoded. URI#path= rejects that outright (URI::InvalidComponentError) rather than escaping it, which .proxy's rescue turned into a silent nil -> every article link 502'd, while the search results page linking to them worked fine (its own path, /kiwix/search, is plain ASCII). --- lib/maptool/kiwix.rb | 9 ++++++++- test/test_kiwix.rb | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/lib/maptool/kiwix.rb b/lib/maptool/kiwix.rb index faf6342..5c10891 100644 --- a/lib/maptool/kiwix.rb +++ b/lib/maptool/kiwix.rb @@ -26,9 +26,16 @@ module Maptool # correct without this having to touch response bodies at all. Returns # nil (halted as a 502 by the caller) rather than raising, same # not-reachable-isn't-exceptional stance as .reachable? above. + # + # path arrives already URL-*decoded* — Sinatra decodes a route's splat + # before handing it over — so an article title with non-ASCII + # characters (e.g. "/kiwix/content/wikipedia/Dlhé_diely") has to be + # re-escaped before URI#path= will accept it at all; it raises + # URI::InvalidComponentError on raw UTF-8 bytes rather than escaping + # them itself. Article links otherwise 502'd for exactly this reason. def self.proxy(base_url, path, query_string, open_timeout: 2, read_timeout: 10) uri = URI(base_url) - uri.path = path + uri.path = URI::RFC2396_PARSER.escape(path) uri.query = query_string unless query_string.to_s.empty? Net::HTTP.start(uri.host, uri.port, open_timeout: open_timeout, read_timeout: read_timeout) { |http| http.request(Net::HTTP::Get.new(uri)) } rescue StandardError diff --git a/test/test_kiwix.rb b/test/test_kiwix.rb index d233aa1..d40ef45 100644 --- a/test/test_kiwix.rb +++ b/test/test_kiwix.rb @@ -45,4 +45,29 @@ class KiwixTest < Minitest::Test def test_proxy_returns_nil_when_nothing_is_listening assert_nil Maptool::Kiwix.proxy("http://127.0.0.1:1", "/kiwix/search", "", open_timeout: 1) end + + # A path with non-ASCII characters, e.g. an article title like + # "/kiwix/content/wikipedia/Dlhé_diely" — exactly what Sinatra's splat + # hands .proxy for a real article link, already URL-*decoded*. Used to + # raise URI::InvalidComponentError (caught, so this returned nil — every + # article link 502'd) since URI#path= rejects raw UTF-8 bytes outright + # instead of escaping them. + def test_proxy_escapes_non_ascii_characters_in_the_path + server = TCPServer.new("127.0.0.1", 0) + port = server.addr[1] + request_line = nil + thread = Thread.new do + client = server.accept + request_line = client.gets + client.print("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 0\r\n\r\n") + client.close + end + + response = Maptool::Kiwix.proxy("http://127.0.0.1:#{port}", "/kiwix/content/wikipedia/Dlhé_diely", "") + thread.join + server.close + + assert_equal "GET /kiwix/content/wikipedia/Dlh%C3%A9_diely HTTP/1.1\r\n", request_line + assert_equal "200", response.code + end end