All-clear image URL contains a literal space; Webex rejects it with HTTP 400 #152

Closed
opened 2026-07-28 02:57:20 +00:00 by claude-bot · 1 comment
Contributor

Root cause of the Webex 400 loop reported in #148. #148 fixed the unbounded retry; this is the underlying rejection.

Diagnosis

_format_iastate_valid (app/services/radar.py:198-205) returns Iowa State's valid segment as 'YYYY-MM-DD HHMM'with a literal space:

return utc.strftime("%Y-%m-%d %H%M")

_iastate_allclear_image_url (app/services/notifiers/webhook.py:66-89) prepends that as a path segment, producing:

https://mesonet.agron.iastate.edu/plotting/auto/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 is not a valid URI, and rejects the whole message with HTTP 400 — which is exactly the failure looping in production on channel e70842aa.

Confirmed empirically

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

Why only the all-clear path

_iastate_alert_image_url builds the same URL without the valid: segment, so it has no space and regular alerts send fine. The valid: segment exists only on the all-clear variant. That asymmetry is why alerts work and all-clears fail, and it is what pointed at the cause.

Why the server-side fetch never noticed

radar.py:234 uses the same helper for fetch_iastate_warning_image, but there the string is passed to httpx, which percent-encodes the space when building the request. Only the "hand the raw string to a third party" path is broken.

Other affected consumers

The same malformed URL is produced for:

  • app/services/notifiers/webex.py:307confirmed hard failure (HTTP 400, message never sent)
  • app/services/notifiers/webhook.py:647 and :671 — Discord embed / webhook payloads
  • app/api/public.py:185 — the public alert page image

Behaviour in the others depends on how tolerant each consumer is (browsers percent-encode a src automatically, so the public page probably renders). Webex is simply the strictest. The URL is malformed regardless and should be fixed at the source.

Fix

Percent-encode the valid value in _iastate_allclear_image_url, the one place that emits a URL string for external consumption. Leave _format_iastate_valid returning the human format — it is documented as such and the server-side fetch path relies on httpx encoding it, so encoding there risks double-encoding.

**Root cause of the Webex 400 loop reported in #148.** #148 fixed the *unbounded retry*; this is the underlying rejection. ## Diagnosis `_format_iastate_valid` (`app/services/radar.py:198-205`) returns Iowa State's valid segment as `'YYYY-MM-DD HHMM'` — **with a literal space**: ```python return utc.strftime("%Y-%m-%d %H%M") ``` `_iastate_allclear_image_url` (`app/services/notifiers/webhook.py:66-89`) prepends that as a path segment, producing: ``` https://mesonet.agron.iastate.edu/plotting/auto/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 is not a valid URI, and rejects the whole message with **HTTP 400** — which is exactly the failure looping in production on channel `e70842aa`. ## Confirmed empirically ``` raw (literal space) : http=000 <- curl cannot even form the request percent-encoded : http=200 image/png 732680 bytes ``` ## Why only the all-clear path `_iastate_alert_image_url` builds the **same** URL without the `valid:` segment, so it has no space and regular alerts send fine. The `valid:` segment exists only on the all-clear variant. That asymmetry is why alerts work and all-clears fail, and it is what pointed at the cause. ## Why the server-side fetch never noticed `radar.py:234` uses the same helper for `fetch_iastate_warning_image`, but there the string is passed to httpx, which percent-encodes the space when building the request. Only the "hand the raw string to a third party" path is broken. ## Other affected consumers The same malformed URL is produced for: - `app/services/notifiers/webex.py:307` — **confirmed hard failure** (HTTP 400, message never sent) - `app/services/notifiers/webhook.py:647` and `:671` — Discord embed / webhook payloads - `app/api/public.py:185` — the public alert page image Behaviour in the others depends on how tolerant each consumer is (browsers percent-encode a `src` automatically, so the public page probably renders). Webex is simply the strictest. The URL is malformed regardless and should be fixed at the source. ## Fix Percent-encode the `valid` value in `_iastate_allclear_image_url`, the one place that emits a URL string for external consumption. Leave `_format_iastate_valid` returning the human format — it is documented as such and the server-side fetch path relies on httpx encoding it, so encoding there risks double-encoding.
Author
Contributor

Fixed in #153 (merged). CI green in 3m6s. 815 SQLite-tier tests pass.

quote(..., safe="") on the valid value at the point of use, so valid:2026-07-24 1716 becomes valid:2026-07-24%201716 and the URL is a proper URI again. Deliberately not applied inside _format_iastate_valid, which the server-side fetch path relies on httpx to encode — doing it there would risk double-encoding.

Tests assert no raw whitespace anywhere in the URL, %20 present, segment separators and the .png suffix intact, and that it parses as a proper URI — plus a control that the alert variant is unchanged and still carries no valid: segment.

Not yet live. See the deployment note on #148: the running stack is not the one CD deploys to, so this fix is on main but not in production.

Fixed in #153 (merged). CI green in 3m6s. 815 SQLite-tier tests pass. `quote(..., safe="")` on the valid value at the point of use, so `valid:2026-07-24 1716` becomes `valid:2026-07-24%201716` and the URL is a proper URI again. Deliberately not applied inside `_format_iastate_valid`, which the server-side fetch path relies on httpx to encode — doing it there would risk double-encoding. Tests assert no raw whitespace anywhere in the URL, `%20` present, segment separators and the `.png` suffix intact, and that it parses as a proper URI — plus a control that the alert variant is unchanged and still carries no `valid:` segment. **Not yet live.** See the deployment note on #148: the running stack is not the one CD deploys to, so this fix is on `main` but not in production.
Sign in to join this conversation.
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#152
No description provided.