refactor(web): drop the unreachable viewport_too_large guard

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.
This commit is contained in:
Graham McIntire 2026-08-05 17:51:55 -05:00
parent 9a3e956cf8
commit 7c37bf67fe
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
4 changed files with 20 additions and 60 deletions

View file

@ -36,7 +36,6 @@ defmodule MicrowavepropWeb.ScoresController do
|> put_resp_header("cache-control", "public, max-age=60") |> put_resp_header("cache-control", "public, max-age=60")
|> send_resp(200, encode_binary(scores)) |> send_resp(200, encode_binary(scores))
else else
{:error, :viewport_too_large} -> payload_too_large(conn)
_ -> bad_request(conn) _ -> bad_request(conn)
end end
end end
@ -49,12 +48,6 @@ defmodule MicrowavepropWeb.ScoresController do
|> json(%{error: "invalid params"}) |> json(%{error: "invalid params"})
end 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(nil), do: {:ok, nil}
defp parse_optional_time(value) when is_binary(value) do defp parse_optional_time(value) when is_binary(value) do

View file

@ -30,7 +30,6 @@ defmodule MicrowavepropWeb.WeatherTileController do
|> put_resp_header("cache-control", "public, max-age=60") |> put_resp_header("cache-control", "public, max-age=60")
|> send_resp(200, encode_binary(rows, layers)) |> send_resp(200, encode_binary(rows, layers))
else else
{:error, :viewport_too_large} -> payload_too_large(conn)
_ -> bad_request(conn) _ -> bad_request(conn)
end end
end end
@ -46,12 +45,6 @@ defmodule MicrowavepropWeb.WeatherTileController do
|> json(%{error: "invalid params"}) |> json(%{error: "invalid params"})
end 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 defp parse_bounds(%{"south" => s, "north" => n, "west" => w, "east" => e}) do
with {:ok, south} <- parse_float(s), with {:ok, south} <- parse_float(s),
{:ok, north} <- parse_float(n), {:ok, north} <- parse_float(n),

View file

@ -4,12 +4,12 @@ defmodule MicrowavepropWeb.GridBounds do
(`/scores/cells`, `/weather/cells`). (`/scores/cells`, `/weather/cells`).
The propagation grid covers HRRR (CONUS) and HRDPS (Canada to The propagation grid covers HRRR (CONUS) and HRDPS (Canada to
60°N): roughly lat 25-60, lon -141 to -52 about 35° × 90° = 60°N): roughly lat 25-60, lon -141 to -52. Any caller-provided
3150 sq degrees. Any caller-provided viewport is clamped to a viewport is clamped to a generous superset of that bbox, which is
generous superset of that bbox, and viewports whose area exceeds what bounds the work: after clamping no request can ask for more
the supported region are rejected as `:viewport_too_large` so a than the full grid (~92k cells, ~1.4 MB), already materialized once
malicious caller cannot trigger global / multi-million-cell per valid_time and served from cache. A global or otherwise absurd
binary responses. viewport therefore costs no more than a fully zoomed-out map does.
`nil` bounds are passed through (caller intends the full grid). `nil` bounds are passed through (caller intends the full grid).
""" """
@ -22,37 +22,17 @@ defmodule MicrowavepropWeb.GridBounds do
@lon_min -150.0 @lon_min -150.0
@lon_max -50.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 @type bounds :: %{optional(String.t()) => float()} | nil
@doc """ @doc """
Clamp `bounds` to the supported grid extent. Returns: Clamp `bounds` to the supported grid extent. Returns:
* `{:ok, nil}` when bounds are nil (caller wants the full grid) * `{:ok, nil}` when bounds are nil (caller wants the full grid)
* `{:ok, clamped_bounds}` when the clamped viewport area is OK * `{:ok, clamped_bounds}` otherwise
* `{:error, :viewport_too_large}` when the clamped area still
exceeds `@max_viewport_area_sq_deg` (catches degenerate
requests like global bounds that survive clamping)
* `{:error, :invalid_bounds}` when north < south or east < west * `{:error, :invalid_bounds}` when north < south or east < west
""" """
@spec clamp(bounds()) :: @spec clamp(bounds()) :: {:ok, bounds()} | {:error, :invalid_bounds}
{:ok, bounds()} | {:error, :viewport_too_large | :invalid_bounds}
def clamp(nil), do: {:ok, nil} def clamp(nil), do: {:ok, nil}
def clamp(%{"south" => s, "north" => n, "west" => w, "east" => e}) 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) west = clamp_value(w, @lon_min, @lon_max)
east = clamp_value(e, @lon_min, @lon_max) east = clamp_value(e, @lon_min, @lon_max)
cond do if north < south or east < west do
north < south or east < west -> {:error, :invalid_bounds}
{:error, :invalid_bounds} else
{:ok, %{"south" => south, "north" => north, "west" => west, "east" => east}}
(north - south) * (east - west) > @max_viewport_area_sq_deg ->
{:error, :viewport_too_large}
true ->
{:ok, %{"south" => south, "north" => north, "west" => west, "east" => east}}
end end
end end
def clamp(_), do: {:error, :invalid_bounds} 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) defp clamp_value(v, lo, hi), do: v |> max(lo) |> min(hi)
end end

View file

@ -30,10 +30,13 @@ defmodule MicrowavepropWeb.GridBoundsTest do
GridBounds.clamp(global) GridBounds.clamp(global)
end end
# The cap is defense-in-depth against the clamp window being widened # The clamp is the only thing bounding request cost, so its output
# later; nothing the clamp can emit today exceeds it. # must never exceed the grid extent no matter how absurd the input.
test "the clamped extent stays inside the area cap" do test "clamped output never escapes the supported extent" do
assert (65.0 - 20.0) * (-50.0 - -150.0) <= GridBounds.max_viewport_area_sq_deg() 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 end
test "rejects inverted bounds" do test "rejects inverted bounds" do