Skip to content

fix(gateway): Weixin cron/notification sends recover from iLink 'prepare failed' instead of tripping the rate-limit breaker (#112709, salvage #112713) - #113069

Merged
teknium1 merged 3 commits into
mainfrom
fix/b113-gateway-platforms-weixin-prepare
Sep 16, 2026
Merged

teknium1 merged 3 commits into
mainfrom
fix/b113-gateway-platforms-weixin-prepare

Conversation

@teknium1

@teknium1 teknium1 commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator

Weixin cron/notification pushes no longer get circuit-broken when iLink answers ret=-2 errmsg="prepare failed" — the adapter now treats it as the stale-session signal it is and re-sends without the peer's context_token, so the message arrives.

  • gateway/platforms/weixin.py::_is_stale_session_ret — recognises prepare failed alongside unknown error as the stale-session variant of iLink's -2 (@KoNit-K, cherry-picked from fix(gateway): retry Weixin prepare failures without token #112713). _send_text_chunk routes through _is_session_expired; the media send path (_send_file) previously did not read ret at all — fixed in b5f7a5a (see Review follow-up).
  • gateway/platforms/weixin.py::_send_text_chunk — the tokenless re-send no longer consumes a retry slot. With send_chunk_retries=0 (the setting the reporter used to surface the raw error) the continue used up the only attempt and send() failed with an empty AssertionError; attempt now counts real failures only. Backoff timings and log counters unchanged.
  • website/docs/user-guide/messaging/weixin.md — troubleshooting row for ret=-2 prepare failed / unknown error.
  • Tests (2): test_prepare_failed_retries_without_context_token (contributor, parametrised over ret/errcode, asserts the circuit stays closed) and test_tokenless_resend_does_not_consume_retry_budget.

Validation (in-process probe, faked iLink response {"ret": -2, "errmsg": "prepare failed"}, send_chunk_retries=0)

base origin/main 9796235 this branch
_is_stale_session_ret(-2, None, "prepare failed") False True
adapter.send() success=False, iLink sendmessage rate limited; cooldown active for 30.0s, 1 call with ctx-token, circuit open 30 s success=True, calls ['ctx-token', None], circuit cooldown 0.0
control: {"ret": -2, "errmsg": "frequency limit"} rate-limit branch, circuit opens rate-limit branch, circuit opens (unchanged)

scripts/run_tests.sh tests/gateway/test_weixin.py → 35 passed; tests/gateway/test_weixin_secret_scope.py tests/gateway/test_weixin_typing.py → 14 passed.

Root cause: _is_stale_session_ret only accepted errmsg == "unknown error", so the prepare failed variant of the stale-context -2 fell into the rate-limit branch (_record_rate_limit_event, threshold 1, 30 s cooldown) before the existing tokenless fallback could run.

Fixes #112709
Salvages #112713 (@KoNit-K)

Infographic

weixin-prepare

Review follow-up

  • [MAJOR] gateway/platforms/weixin.py::_send_file ignored iLink ret/errmsg; ret=-2 prepare failed on a cron/notification media attachment reported success=True with no tokenless re-send — confirmed on 516bdba (probe: mocked _api_request → {"ret": -2, "errmsg": "prepare failed"}, send_document(..., caption="hi") → success=True, 2 calls, both with context_token). Fixed @b5f7a5afa0f89: _send_file now checks ret/errcode on the caption and media item sends, re-sends once without context_token (dropping the cached token) via the same _is_session_expired check _send_text_chunk uses, and raises on any other non-zero response so _send_file_result returns success=False. Invariant test test_media_send_reads_ret_and_resends_without_token_on_stale_session (red without the fix). Same probe after the fix: success=False error="iLink sendmessage error: ret=-2 errcode=None errmsg=prepare failed", tokens ["ctx-token", None]. The incorrect "one site covers both" sentence above is corrected.
  • [MINOR] zh-Hans mirror of the troubleshooting row missing — fixed @b5f7a5afa0f89 (website/i18n/zh-Hans/.../messaging/weixin.md, same table).

KoNit-K and others added 2 commits September 16, 2026 09:50
…udget

The stale-session fallback in `_send_text_chunk` re-sends without
`context_token` via `continue`, which advanced the `for attempt in
range(retries + 1)` loop. With `send_chunk_retries=0` (the setting the
reporter used to surface the raw iLink error) the re-send never happened:
the loop ended with `last_error is None` and `send()` failed with an empty
AssertionError. Count only real failures in `attempt` so the tokenless
re-send is free; backoff timings and log counters are unchanged.

Also documents the `ret=-2 prepare failed` / `unknown error` stale-session
behaviour in the Weixin troubleshooting table.

Part of #112709
@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

૮ >ﻌ< ა ci review

ran on b5f7a5a — fix(gateway): Weixin media sends honour iLink ret and re-sen

debug info

CI timings

CI timings · View report · View job

Wall time 6m16s vs 6m8s (+2.2%). 7 job(s) slower, 4 faster, 2 unchanged.

  • Docs Site / docs-site-checks: -44.0s
  • Python tests / Run tests: +14.0s
  • Check contributors / check-attribution: -14.0s
  • Python tests / e2e: +7.0s
  • Check no case-colliding filenames / check-case-collisions: +6.0s

… a stale token

`_send_file` (send_document / send_image / send_video / send_voice, i.e.
the cron `media_files` leg of `_deliver_direct`) awaited the caption and
media `sendmessage` calls without reading `ret`/`errcode`. `_api_request`
only raises on non-2xx HTTP, so a stale-token `ret=-2 errmsg=prepare
failed` (or `errcode=-14`) came back as `success=True` with the token still
attached and the attachment silently never arrived — the previous commit's
claim that the media path shared `_send_text_chunk`'s `_is_session_expired`
handling was wrong; only the text path had it.

Apply the same mechanism inline in `_send_file`: check `ret`/`errcode` on
every item-list send, re-send once without `context_token` (and drop the
cached token) when `_is_session_expired` says so, and raise on any other
non-zero response so `_send_file_result` reports the failure. Clearing the
token covers both the caption and the media item, and bounds the loop.

Also mirrors the `prepare failed` troubleshooting row into the zh-Hans docs.

Part of #112709
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery platform/wecom WeCom / WeChat Work adapter sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Sep 16, 2026
@teknium1
teknium1 merged commit 97066a0 into main Sep 16, 2026
36 checks passed
@teknium1
teknium1 deleted the fix/b113-gateway-platforms-weixin-prepare branch September 16, 2026 23:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/wecom WeCom / WeChat Work adapter sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Weixin: iLink "prepare failed" misclassified as rate limit, breaking cron/notification delivery

3 participants