Some checks failed
Build and Push / Build CI test image (push) Successful in 20s
Build base image / Build and push base image (push) Successful in 26s
Build prop-grid-rs / Test, build, push (push) Failing after 2m53s
Build and Push / Build and Push Docker Image (push) Has been cancelled
The map sets minZoom: 4, and a z=4 viewport on a wide window clamps to the full grid extent (45° lat × 100° lon = 4500 sq deg), which exceeded the 4000 sq deg cap. The request 413'd and the hook dropped it silently, so zooming all the way out blanked the overlay. Raise the cap to 4600 — just above the largest area the clamp can emit. The clamp, not this cap, is what bounds the work: post-clamp no request can exceed the full grid (~92k cells, ~1.4 MB), which is already materialized once per valid_time and served from cache. The cap stays as defense-in-depth against the clamp window being widened later. Consequence: :viewport_too_large is now unreachable via clamp/1, so the tests that pinned global bounds to a 413 are inverted to assert the clamped extent is served.
84 lines
3.3 KiB
Elixir
84 lines
3.3 KiB
Elixir
defmodule MicrowavepropWeb.GridBounds do
|
||
@moduledoc """
|
||
Bounds clamp + sanity checks for grid-cell HTTP endpoints
|
||
(`/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.
|
||
|
||
`nil` bounds are passed through (caller intends the full grid).
|
||
"""
|
||
|
||
# Generous superset of HRRR + HRDPS — a few extra degrees on each
|
||
# edge so legitimate clients zoomed slightly past the data extent
|
||
# still get a 200 with the actual covered cells.
|
||
@lat_min 20.0
|
||
@lat_max 65.0
|
||
@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)
|
||
* `{:error, :invalid_bounds}` when north < south or east < west
|
||
"""
|
||
@spec clamp(bounds()) ::
|
||
{:ok, bounds()} | {:error, :viewport_too_large | :invalid_bounds}
|
||
def clamp(nil), do: {:ok, nil}
|
||
|
||
def clamp(%{"south" => s, "north" => n, "west" => w, "east" => e})
|
||
when is_number(s) and is_number(n) and is_number(w) and is_number(e) do
|
||
south = clamp_value(s, @lat_min, @lat_max)
|
||
north = clamp_value(n, @lat_min, @lat_max)
|
||
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}}
|
||
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
|