Webex all-clear 400s in an endless retry loop; response body not logged #148
Labels
No labels
area:ai
area:ci-cd
area:notifications
area:observability
area:public-pages
backlog
bug
duplicate
enhancement
help wanted
invalid
question
type:decision
type:feature
type:infra
type:maintenance
type:security
v1.0.1
v1.1.0
v1.2.0
v1.3.0
v2.0.0
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/WeatherBot#148
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Observed live in
weatherbot-app-1logs on 2026-07-27 (continuous through the whole retained log window, roughly once a minute):Two separate problems:
1. A
400is being retried forever. HTTP 400 is a permanent client error — the request is malformed or references something that no longer exists (badroomId, deleted room, revoked token). Retrying it cannot succeed. The delivery outbox hasPermanentDeliveryErrorand adeadstatus precisely for this; either the Webex notifier is not raisingPermanentDeliveryErrorfor 4xx, or the all-clear path bypasses the outbox classification. A permanent failure should be marked dead after one attempt, not looped.2. The response body is discarded. The log records only the status code. Webex returns a JSON body with a specific
messageand atrackingId, which is what would actually identify the cause. Without it the failure is undiagnosable from logs alone — the reason this needs a live reproduction rather than a log read.Tasks
PermanentDeliveryErrorso it goesdeadrather than retrying indefinitely.trackingIdon failure, redacting the token.Note the retry loop also means the
sent_alertsrow keepslifted_notified_atNULL indefinitely, sosend_pending_lifted_notificationsre-selects it every cycle.Fixed in #151 (merged). CI green.
Root cause was one layer above where I expected. The classification was already right —
classify_http_errormaps a 4xx other than 408/429 toPermanentDeliveryError, andwebex.send_alert_liftedraises it. But_dispatch_alert_liftedcaught every exception and returned a bareFalse, discarding the distinction, and the caller only stampedlifted_notified_aton success. So the row stayed pending and was re-selected every cycle forever.This was never Webex-specific.
_dispatch_alert_liftedis the shared dispatch for every channel type, so the same unbounded loop applied to all of them — which resolves the "check the other notifiers" task: there was one pattern, in one place.Now:
_dispatch_alert_liftedreturns(sent, permanent);record_delivery_resulttakespermanent=Trueand marks the ledger row dead immediately; the loop gives up once the row isdead, whether from a permanent rejection or from transient failures exhaustingdelivery_max_attempts/delivery_max_age_hours. Transient failures still retry — they just stop. No new config; this reuses bounds the outbox already had and the all-clear path was simply not consulting.The Webex response body and
trackingIdare now logged on rejection (response only — the bot token is in the request headers and never echoed back). That is what will tell you why channele70842aais 400ing, which I could not determine from the code; a deleted room or revoked token are the usual causes, but the body will say.On deploy: this does not retroactively clear the stuck row. It still has
lifted_notified_atNULL, so it will make one final attempt, fail, log the body, and be marked dead — after which the log goes quiet and the delivery row carries the reason. Worth grabbing that one log line when it appears.Follow-up: the underlying cause is identified and fixed in #152 / #153.
The Webex 400 was not a bad room id or a revoked token.
_iastate_allclear_image_urlwas emitting a URL containing a literal space — Iowa State's valid segment is'YYYY-MM-DD HHMM'— and Webex validatesfiles[]URLs, so it rejected the whole all-clear:Confirmed:
curlcannot even form a request for the raw URL (http=000); percent-encoded it returns200 image/png.Found by code inspection rather than from the new log line, via the asymmetry this issue already noted — ordinary alerts sent fine, only all-clears failed.
_iastate_alert_image_urlhas novalid:segment and therefore no space. The response-body logging added here would have shown it directly, and is still worth having.So the two fixes are complementary and both wanted: #153 stops the rejection, this one stops a permanent rejection from looping forever if anything else ever produces one.
Deployment note (applies to #145, #146, #147, #148 and #152).
None of these fixes are live yet. The running stack (
weatherbot-app-1, compose project dir/home/ryan/WeatherBot) is not what.forgejo/workflows/cd.ymldeploys to — CD doescd ~/projects/WeatherBoton${DEV_HOST:-claude@10.1.1.14}, a different checkout.Evidence: CD run #5039 for the #148 merge succeeded at 02:13, and at 02:46–02:48 the app was still logging the pre-fix message format (
Webex all-clear failed ... Client error '400 Bad Request', with noWebex API rejected the messageline and noGiving up on all-clear). The loop had also grown from ~1/min to 2–3/min, so more than one all-clear row is now stuck.Either the deploy step is being skipped (
DEV_SSH_KEYunset — the workflow is written to pass silently in that case) or it is deploying to a different instance. Worth resolving, since a green CD run currently implies a deploy that may not be happening.