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")
|> 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

View file

@ -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),

View file

@ -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

View file

@ -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