diff --git a/lib/maptool/overpass_pois.rb b/lib/maptool/overpass_pois.rb index fbe47e9..39e0dc4 100644 --- a/lib/maptool/overpass_pois.rb +++ b/lib/maptool/overpass_pois.rb @@ -12,7 +12,15 @@ module Maptool module OverpassPois class FetchError < StandardError; end - OVERPASS_API = "https://overpass-api.de/api/interpreter".freeze + # Same three public mirrors public/js/hiking_trails.js races — tried in + # turn here instead, falling through to the next on any failure: the + # shared free instances regularly 504/429 under load, and a plugin run + # is a one-off click, not worth threads to race them. + OVERPASS_APIS = [ + "https://overpass-api.de/api/interpreter", + "https://overpass.kumi.systems/api/interpreter", + "https://overpass.private.coffee/api/interpreter", + ].freeze # Each entry: an Overpass tag filter -> { label:, icon:, color: }, used # both to build the query (unioning every filter) and to label/style @@ -53,12 +61,23 @@ module Maptool # bbox: [south, west, north, east] (Overpass's own axis order). def self.fetch(bbox, timeout: 60) query = build_query(bbox, timeout: timeout) - uri = URI(OVERPASS_API) + errors = OVERPASS_APIS.map do |url| + return post(URI(url), query, timeout) + rescue FetchError, Net::OpenTimeout, Net::ReadTimeout, SystemCallError, SocketError, OpenSSL::SSL::SSLError, JSON::ParserError => e + "#{URI(url).host}: #{e.message}" + end + raise FetchError, errors.join("; ") + end + + def self.post(uri, query, timeout) # overpass-api.de answers Net::HTTP's default "Ruby" User-Agent with # a 406 (verified 2026-10-09) — any identifying UA gets through. request = Net::HTTP::Post.new(uri, "User-Agent" => "maptool (Overpass camper POIs plugin)") request.set_form_data("data" => query) - response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) } + # read_timeout past the query's own [timeout:] — Net::HTTP's default + # 60s equals it, so a big viewport could hit Net::ReadTimeout just as + # Overpass was about to answer (a country-sized bbox measured ~57s). + response = Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: timeout + 30) { |http| http.request(request) } raise FetchError, "unexpected response #{response.code}" unless response.is_a?(Net::HTTPSuccess) JSON.parse(response.body) diff --git a/lib/maptool/plugins.rb b/lib/maptool/plugins.rb index bf86064..a67a294 100644 --- a/lib/maptool/plugins.rb +++ b/lib/maptool/plugins.rb @@ -65,8 +65,13 @@ module Maptool south, west, north, east = bbox args = ["--south=#{south}", "--west=#{west}", "--north=#{north}", "--east=#{east}"] - stdout, _stderr, status = Open3.capture3(command, *args, chdir: plugin_dir) - raise RunError, "plugin exited with status #{status.exitstatus}" unless status.success? + stdout, stderr, status = Open3.capture3(command, *args, chdir: plugin_dir) + # First stderr line too — for a Ruby plugin that's the exception + # message (e.g. "...: unexpected response 406"), the only clue to why. + unless status.success? + reason = stderr.lines.first&.strip + raise RunError, ["plugin exited with status #{status.exitstatus}", reason].compact.reject(&:empty?).join(": ") + end geojson = JSON.parse(stdout) raise RunError, "plugin did not print a GeoJSON FeatureCollection" unless geojson.is_a?(Hash) && geojson["type"] == "FeatureCollection"