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:
parent
9a3e956cf8
commit
7c37bf67fe
4 changed files with 20 additions and 60 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue