From 7c37bf67fea0317dc25821845c0620f689dfdab6 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Wed, 5 Aug 2026 17:51:55 -0500 Subject: [PATCH] refactor(web): drop the unreachable viewport_too_large guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Raising the area cap to 4600 put it above the largest area clamp/1 can emit (45° lat × 100° lon = 4500 sq deg), so :viewport_too_large became unreachable and the 413 handlers in both cell controllers became dead code. Remove the cap, the error branch, the max_viewport_area_sq_deg/0 accessor and both payload_too_large/1 handlers. The clamp is what bounds request cost: post-clamp nothing can ask for more than the full grid (~92k cells, ~1.4 MB), which is materialized once per valid_time and served from cache, so a global viewport costs no more than a fully zoomed-out map. Replaces the cap test with one asserting the clamp never lets absurd input escape the supported extent — the property the guard was standing in for. --- .../controllers/scores_controller.ex | 7 --- .../controllers/weather_tile_controller.ex | 7 --- lib/microwaveprop_web/grid_bounds.ex | 55 +++++-------------- test/microwaveprop_web/grid_bounds_test.exs | 11 ++-- 4 files changed, 20 insertions(+), 60 deletions(-) diff --git a/lib/microwaveprop_web/controllers/scores_controller.ex b/lib/microwaveprop_web/controllers/scores_controller.ex index 9a11fd6e..1982ba5b 100644 --- a/lib/microwaveprop_web/controllers/scores_controller.ex +++ b/lib/microwaveprop_web/controllers/scores_controller.ex @@ -36,7 +36,6 @@ defmodule MicrowavepropWeb.ScoresController do |> put_resp_header("cache-control", "public, max-age=60") |> send_resp(200, encode_binary(scores)) else - {:error, :viewport_too_large} -> payload_too_large(conn) _ -> bad_request(conn) end end @@ -49,12 +48,6 @@ defmodule MicrowavepropWeb.ScoresController do |> json(%{error: "invalid params"}) end - defp payload_too_large(conn) do - conn - |> put_status(413) - |> json(%{error: "viewport_too_large", detail: "Requested bounds exceed the maximum supported viewport area."}) - end - defp parse_optional_time(nil), do: {:ok, nil} defp parse_optional_time(value) when is_binary(value) do diff --git a/lib/microwaveprop_web/controllers/weather_tile_controller.ex b/lib/microwaveprop_web/controllers/weather_tile_controller.ex index 353c8bf1..c4bcf674 100644 --- a/lib/microwaveprop_web/controllers/weather_tile_controller.ex +++ b/lib/microwaveprop_web/controllers/weather_tile_controller.ex @@ -30,7 +30,6 @@ defmodule MicrowavepropWeb.WeatherTileController do |> put_resp_header("cache-control", "public, max-age=60") |> send_resp(200, encode_binary(rows, layers)) else - {:error, :viewport_too_large} -> payload_too_large(conn) _ -> bad_request(conn) end end @@ -46,12 +45,6 @@ defmodule MicrowavepropWeb.WeatherTileController do |> json(%{error: "invalid params"}) end - defp payload_too_large(conn) do - conn - |> put_status(413) - |> json(%{error: "viewport_too_large", detail: "Requested bounds exceed the maximum supported viewport area."}) - end - defp parse_bounds(%{"south" => s, "north" => n, "west" => w, "east" => e}) do with {:ok, south} <- parse_float(s), {:ok, north} <- parse_float(n), diff --git a/lib/microwaveprop_web/grid_bounds.ex b/lib/microwaveprop_web/grid_bounds.ex index b6fd215d..13644d55 100644 --- a/lib/microwaveprop_web/grid_bounds.ex +++ b/lib/microwaveprop_web/grid_bounds.ex @@ -4,12 +4,12 @@ defmodule MicrowavepropWeb.GridBounds do (`/scores/cells`, `/weather/cells`). The propagation grid covers HRRR (CONUS) and HRDPS (Canada to - 60°N): roughly lat 25-60, lon -141 to -52 — about 35° × 90° = - 3150 sq degrees. Any caller-provided viewport is clamped to a - generous superset of that bbox, and viewports whose area exceeds - the supported region are rejected as `:viewport_too_large` so a - malicious caller cannot trigger global / multi-million-cell - binary responses. + 60°N): roughly lat 25-60, lon -141 to -52. Any caller-provided + viewport is clamped to a generous superset of that bbox, which is + what bounds the work: after clamping no request can ask for more + than the full grid (~92k cells, ~1.4 MB), already materialized once + per valid_time and served from cache. A global or otherwise absurd + viewport therefore costs no more than a fully zoomed-out map does. `nil` bounds are passed through (caller intends the full grid). """ @@ -22,37 +22,17 @@ defmodule MicrowavepropWeb.GridBounds do @lon_min -150.0 @lon_max -50.0 - # Sits just above the largest area the clamp above can produce - # (45° lat × 100° lon = 4500 sq deg), so a fully zoomed-out client - # gets the whole grid rather than a 413. - # - # The clamp — not this cap — is what bounds the work: after clamping, - # no request can ask for more than the full grid (~92k cells, ~1.4 MB), - # which the pipeline already materializes once per valid_time and - # serves from cache. An earlier 4000.0 was tuned to reject the - # full-extent request as a DoS shape, but that is also exactly what - # the map requests at its own `minZoom: 4`, so it silently blanked - # the overlay whenever a user zoomed all the way out. - # - # Kept as defense-in-depth: if the clamp window is ever widened (e.g. - # extending coverage to Alaska), this catches the resulting blow-up - # instead of letting response size grow unbounded. - @max_viewport_area_sq_deg 4600.0 - @type bounds :: %{optional(String.t()) => float()} | nil @doc """ Clamp `bounds` to the supported grid extent. Returns: * `{:ok, nil}` when bounds are nil (caller wants the full grid) - * `{:ok, clamped_bounds}` when the clamped viewport area is OK - * `{:error, :viewport_too_large}` when the clamped area still - exceeds `@max_viewport_area_sq_deg` (catches degenerate - requests like global bounds that survive clamping) + * `{:ok, clamped_bounds}` otherwise * `{:error, :invalid_bounds}` when north < south or east < west + """ - @spec clamp(bounds()) :: - {:ok, bounds()} | {:error, :viewport_too_large | :invalid_bounds} + @spec clamp(bounds()) :: {:ok, bounds()} | {:error, :invalid_bounds} def clamp(nil), do: {:ok, nil} def clamp(%{"south" => s, "north" => n, "west" => w, "east" => e}) @@ -62,23 +42,14 @@ defmodule MicrowavepropWeb.GridBounds do west = clamp_value(w, @lon_min, @lon_max) east = clamp_value(e, @lon_min, @lon_max) - cond do - north < south or east < west -> - {:error, :invalid_bounds} - - (north - south) * (east - west) > @max_viewport_area_sq_deg -> - {:error, :viewport_too_large} - - true -> - {:ok, %{"south" => south, "north" => north, "west" => west, "east" => east}} + if north < south or east < west do + {:error, :invalid_bounds} + else + {:ok, %{"south" => south, "north" => north, "west" => west, "east" => east}} end end def clamp(_), do: {:error, :invalid_bounds} - @doc "Maximum viewport area in square degrees." - @spec max_viewport_area_sq_deg() :: float() - def max_viewport_area_sq_deg, do: @max_viewport_area_sq_deg - defp clamp_value(v, lo, hi), do: v |> max(lo) |> min(hi) end diff --git a/test/microwaveprop_web/grid_bounds_test.exs b/test/microwaveprop_web/grid_bounds_test.exs index 5b7c6430..1436138a 100644 --- a/test/microwaveprop_web/grid_bounds_test.exs +++ b/test/microwaveprop_web/grid_bounds_test.exs @@ -30,10 +30,13 @@ defmodule MicrowavepropWeb.GridBoundsTest do GridBounds.clamp(global) end - # The cap is defense-in-depth against the clamp window being widened - # later; nothing the clamp can emit today exceeds it. - test "the clamped extent stays inside the area cap" do - assert (65.0 - 20.0) * (-50.0 - -150.0) <= GridBounds.max_viewport_area_sq_deg() + # The clamp is the only thing bounding request cost, so its output + # must never exceed the grid extent no matter how absurd the input. + test "clamped output never escapes the supported extent" do + absurd = %{"south" => -1.0e6, "north" => 1.0e6, "west" => -1.0e6, "east" => 1.0e6} + + assert {:ok, %{"south" => 20.0, "north" => 65.0, "west" => -150.0, "east" => -50.0}} = + GridBounds.clamp(absurd) end test "rejects inverted bounds" do