Percent-encode the all-clear image URL (#152) #153

Merged
claude-bot merged 1 commit from fix/allclear-image-url-space into main 2026-07-28 03:13:57 +00:00
Contributor

Closes #152. Root cause of the Webex HTTP 400 loop reported in #148 — that issue fixed the unbounded retry; this fixes the rejection.

Diagnosis

Iowa State's valid path segment is 'YYYY-MM-DD HHMM' — it contains a space. _iastate_allclear_image_url embedded it raw, producing a URL that is not a valid URI:

.../plot/208/valid:2026-07-24 1716::network:WFO::wfo:LSX::...png
                            ^ raw space

That string is handed to Webex as a files[] URL. Webex validates it, finds it malformed, and rejects the entire message with HTTP 400.

Confirmed empirically:

raw (literal space) : http=000   <- curl cannot even form the request
percent-encoded     : http=200   image/png   732680 bytes

Why this was hard to spot

Three things hid it:

  • Ordinary alerts worked. _iastate_alert_image_url builds the same URL without the valid: segment, so it has no space. That asymmetry — alerts fine, all-clears failing — is what identified the cause.
  • The server-side fetch worked. radar.fetch_iastate_warning_image uses the same helper but passes the string to httpx, which percent-encodes it. Only URLs handed verbatim to a third party break.
  • The status code alone said nothing. Webex's explanation is in the response body, which was not logged — fixed separately in #148.

Other consumers

The same malformed URL also reaches webhook.py:647 / :671 (Discord embed and webhook payloads) and app/api/public.py:185 (public alert page). Those fail less visibly — browsers percent-encode a src automatically — but the URL is malformed regardless, so fixing it at the source repairs all of them.

Fix

quote(..., safe="") on the valid value, at the point of use. Deliberately not inside _format_iastate_valid: that helper is documented as returning the human-readable format and the fetch path relies on httpx encoding it, so encoding there would risk double-encoding.

Tests

tests/test_iastate_image_urls.py — no raw space, %20 present, segment separators and .png suffix intact, URL parses as a proper URI with no whitespace anywhere, plus a control that the alert variant is unchanged and still has no valid: segment, and the two None guards.

Verification

Step Result
ruff check . passed
python -m compileall app OK
SQLite bulk tier 815 passed
Live URL check raw 000 → encoded 200 image/png

🤖 Generated with Claude Code

Closes #152. Root cause of the Webex HTTP 400 loop reported in #148 — that issue fixed the unbounded *retry*; this fixes the *rejection*. ## Diagnosis Iowa State's `valid` path segment is `'YYYY-MM-DD HHMM'` — it contains a **space**. `_iastate_allclear_image_url` embedded it raw, producing a URL that is not a valid URI: ``` .../plot/208/valid:2026-07-24 1716::network:WFO::wfo:LSX::...png ^ raw space ``` That string is handed to Webex as a `files[]` URL. Webex validates it, finds it malformed, and rejects the **entire message** with HTTP 400. Confirmed empirically: ``` raw (literal space) : http=000 <- curl cannot even form the request percent-encoded : http=200 image/png 732680 bytes ``` ## Why this was hard to spot Three things hid it: - **Ordinary alerts worked.** `_iastate_alert_image_url` builds the same URL *without* the `valid:` segment, so it has no space. That asymmetry — alerts fine, all-clears failing — is what identified the cause. - **The server-side fetch worked.** `radar.fetch_iastate_warning_image` uses the same helper but passes the string to httpx, which percent-encodes it. Only URLs handed verbatim to a third party break. - **The status code alone said nothing.** Webex's explanation is in the response body, which was not logged — fixed separately in #148. ## Other consumers The same malformed URL also reaches `webhook.py:647` / `:671` (Discord embed and webhook payloads) and `app/api/public.py:185` (public alert page). Those fail less visibly — browsers percent-encode a `src` automatically — but the URL is malformed regardless, so fixing it at the source repairs all of them. ## Fix `quote(..., safe="")` on the valid value, at the point of use. Deliberately **not** inside `_format_iastate_valid`: that helper is documented as returning the human-readable format and the fetch path relies on httpx encoding it, so encoding there would risk double-encoding. ## Tests `tests/test_iastate_image_urls.py` — no raw space, `%20` present, segment separators and `.png` suffix intact, URL parses as a proper URI with no whitespace anywhere, plus a **control** that the alert variant is unchanged and still has no `valid:` segment, and the two `None` guards. ## Verification | Step | Result | |---|---| | `ruff check .` | passed | | `python -m compileall app` | OK | | SQLite bulk tier | 815 passed | | Live URL check | raw `000` → encoded `200 image/png` | 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Percent-encode the all-clear image URL (#152)
All checks were successful
CI / test (pull_request) Successful in 3m6s
d5bac5b215
Root cause of the Webex HTTP 400 loop reported in #148. That issue fixed the
unbounded retry; this fixes the rejection itself.

Iowa State's valid path segment is 'YYYY-MM-DD HHMM' -- it contains a SPACE.
_iastate_allclear_image_url embedded it raw, so the URL it produced was not a
valid URI:

  .../plot/208/valid:2026-07-24 1716::network:WFO::wfo:LSX::...png

That string is handed to Webex as a files[] URL. Webex validates it, finds it
malformed, and rejects the entire all-clear with HTTP 400. Confirmed: curl
cannot even form a request for the raw URL (http=000), while the
percent-encoded form returns 200 image/png.

Ordinary alerts were unaffected because _iastate_alert_image_url has no
`valid:` segment and therefore no space -- that asymmetry is what identified
the cause. The server-side fetch in radar.py never noticed either, because it
passes the string to httpx, which encodes it; only URLs handed verbatim to a
third party are affected. The same malformed URL also reaches the Discord and
webhook payload builders and the public alert page, where it fails less
visibly.

Encoded at the point of use rather than inside _format_iastate_valid, which is
documented as returning the human format and is relied on by the fetch path --
encoding there risks double-encoding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/WeatherBot!153
No description provided.