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")
|
|> 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
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue