fix(weather): serve the fully zoomed-out viewport instead of 413ing it
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
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.
This commit is contained in:
parent
bff90da60e
commit
9a3e956cf8
4 changed files with 47 additions and 14 deletions
|
|
@ -22,11 +22,22 @@ defmodule MicrowavepropWeb.GridBounds do
|
|||
@lon_min -150.0
|
||||
@lon_max -50.0
|
||||
|
||||
# Comfortably above the HRDPS+HRRR union (~3150 sq deg), but tight
|
||||
# enough that the fully-clamped extent (~4500 sq deg) still trips
|
||||
# the cap — a "give me everything" request is exactly the DoS
|
||||
# shape we want to reject.
|
||||
@max_viewport_area_sq_deg 4000.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
|
||||
|
||||
|
|
|
|||
|
|
@ -101,10 +101,16 @@ defmodule MicrowavepropWeb.ScoresControllerTest do
|
|||
assert json_response(conn, 400) == %{"error" => "invalid params"}
|
||||
end
|
||||
|
||||
test "returns 413 for global / oversized viewport bounds", %{conn: conn} do
|
||||
# Mirrors /weather/cells: a fully zoomed-out viewport clamps to the
|
||||
# supported extent and is served rather than rejected.
|
||||
test "serves global / oversized viewport bounds by clamping them", %{conn: conn} do
|
||||
valid_time = ~U[2026-04-28 12:00:00Z]
|
||||
ScoresFile.write!(10_000, valid_time, [%{lat: 33.0, lon: -97.0, score: 75}])
|
||||
|
||||
conn = get(conn, ~p"/scores/cells?band=10000&south=-90&north=90&west=-180&east=180")
|
||||
body = json_response(conn, 413)
|
||||
assert body["error"] == "viewport_too_large"
|
||||
|
||||
assert conn.status == 200
|
||||
assert <<"PSCR", 1::8, _reserved::24, 1::little-32, _rest::binary>> = conn.resp_body
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -169,7 +169,10 @@ defmodule MicrowavepropWeb.WeatherTileControllerTest do
|
|||
File.rm_rf!(hrdps_dir)
|
||||
end
|
||||
|
||||
test "returns 413 for global / oversized viewport bounds", %{conn: conn} do
|
||||
# A fully zoomed-out map (minZoom: 4) sends bounds well past the grid
|
||||
# extent. Those clamp down to the supported region and are served —
|
||||
# rejecting them left the overlay blank with no feedback.
|
||||
test "serves global / oversized viewport bounds by clamping them", %{conn: conn} do
|
||||
time = DateTime.to_iso8601(~U[2026-04-28 12:00:00Z])
|
||||
|
||||
conn =
|
||||
|
|
@ -178,8 +181,8 @@ defmodule MicrowavepropWeb.WeatherTileControllerTest do
|
|||
~p"/weather/cells?time=#{time}&south=-90&north=90&west=-180&east=180"
|
||||
)
|
||||
|
||||
body = json_response(conn, 413)
|
||||
assert body["error"] == "viewport_too_large"
|
||||
assert conn.status == 200
|
||||
assert <<"WCEL", 1::8, _rest::binary>> = conn.resp_body
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,12 +15,25 @@ defmodule MicrowavepropWeb.GridBoundsTest do
|
|||
|
||||
test "clamps south/north/west/east to the supported extent" do
|
||||
bounds = %{"south" => -50.0, "north" => 90.0, "west" => -200.0, "east" => 50.0}
|
||||
assert {:error, :viewport_too_large} = GridBounds.clamp(bounds)
|
||||
|
||||
assert {:ok, %{"south" => 20.0, "north" => 65.0, "west" => -150.0, "east" => -50.0}} =
|
||||
GridBounds.clamp(bounds)
|
||||
end
|
||||
|
||||
test "rejects global bounds with 413-equivalent error" do
|
||||
# The map's own `minZoom: 4` produces a viewport that clamps to the
|
||||
# full extent. Rejecting it blanked the overlay whenever a user
|
||||
# zoomed all the way out, so the clamped full grid must be served.
|
||||
test "serves global bounds by clamping them to the supported extent" do
|
||||
global = %{"south" => -90.0, "north" => 90.0, "west" => -180.0, "east" => 180.0}
|
||||
assert GridBounds.clamp(global) == {:error, :viewport_too_large}
|
||||
|
||||
assert {:ok, %{"south" => 20.0, "north" => 65.0, "west" => -150.0, "east" => -50.0}} =
|
||||
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()
|
||||
end
|
||||
|
||||
test "rejects inverted bounds" do
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue