Skip to content

Commit 5dbb1f5

Browse files
committed
Check the inner status envelope, not only the outer
The API answers with two envelopes: body.status reports that the request reached the instance, body.data.status reports whether the instance carried the action out. The inner one is authoritative. Only the outer was checked, so a malformed chatId -- {"status": "success", "data": {"status": "error", "message": "incorrect chatId format."}} -- returned as a successful send. Nothing was delivered and the caller had no way to know. Verified against the live API on send-message, download-media, get-message-by-id, send-seen, get-profile-pic-url and fetch-messages: all six report input errors this way. A data array, which several actions return, has no status and passes the check untouched.
1 parent 28d8eae commit 5dbb1f5

3 files changed

Lines changed: 57 additions & 13 deletions

File tree

‎README.md‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,18 @@ async with AsyncWaAPI(token="YOUR_API_TOKEN", instance_id=123) as client:
5151

5252
## Errors
5353

54-
A successful HTTP exchange is not proof the message was sent. The API answers
55-
`200` with `{"status": "error"}` when, for example, the instance is not
56-
connected — so the SDK raises on that too, rather than handing back a body that
57-
looks like success.
54+
A successful HTTP exchange is not proof the message was sent. The API carries
55+
two envelopes:
56+
57+
| Field | Answers |
58+
|---|---|
59+
| `status` | did the request reach the instance |
60+
| `data.status` | did the instance carry the action out |
61+
62+
**The inner one is authoritative.** A malformed chat ID comes back as
63+
`{"status": "success", "data": {"status": "error", …}}` — nothing was sent. The
64+
SDK raises `FailedActionError` on either envelope, so neither case reaches you
65+
looking like success.
5866

5967
```python
6068
from waapi import WaAPI, FailedActionError, AuthenticationError, RateLimitError

‎src/waapi/_http.py‎

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,20 +73,33 @@ def interpret(response: httpx.Response, *, check_body_status: bool = True) -> An
7373
if not 200 <= status < 300:
7474
raise WaAPIError(f"WaAPI returned HTTP {status}: {_brief(body)}")
7575

76-
# A 2xx is not proof the action happened. The API answers 200 with
77-
# {"status": "error"} when, for example, the instance is not connected.
76+
# Two envelopes, and the inner one is the authoritative answer:
77+
#
78+
# body.status did the request reach the instance
79+
# body.data.status did the instance carry the action out
80+
#
81+
# A malformed chatId comes back as {"status": "success", "data":
82+
# {"status": "error", "message": "incorrect chatId format."}} -- nothing
83+
# was sent. Checking only the outer envelope reports that as delivered,
84+
# which is the exact failure this class exists to prevent, one level down.
7885
if check_body_status and isinstance(body, Mapping):
79-
envelope = body.get("status")
80-
if envelope is not None and envelope != "success":
81-
raise FailedActionError(
82-
"WaAPI accepted the request but did not carry the action out: "
83-
+ _brief(body),
84-
body,
85-
)
86+
for envelope in (_inner_status(body), body.get("status")):
87+
if envelope is not None and envelope != "success":
88+
raise FailedActionError(
89+
"WaAPI accepted the request but did not carry the action out: "
90+
+ _brief(body),
91+
body,
92+
)
8693

8794
return body
8895

8996

97+
def _inner_status(body: Mapping[str, Any]) -> Any:
98+
"""``body.data.status`` when the response wraps a client action."""
99+
data = body.get("data")
100+
return data.get("status") if isinstance(data, Mapping) else None
101+
102+
90103
def _brief(body: Any, limit: int = 400) -> str:
91104
text = body if isinstance(body, str) else _json.dumps(body, ensure_ascii=False)
92105
return text if len(text) <= limit else text[:limit] + "..."

‎tests/test_client.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,26 @@ def test_the_generated_surface_is_not_empty():
212212

213213
methods = {n for n in vars(GeneratedActions) if not n.startswith("_")}
214214
assert len(methods) > 100, f"only {len(methods)} generated methods"
215+
216+
217+
def test_the_inner_status_is_the_authoritative_one():
218+
"""A request can reach the instance and still not be carried out.
219+
220+
The outer envelope reports delivery to the instance; the inner one reports
221+
whether the instance did the thing. A malformed chatId produces
222+
success-outside, error-inside — and was reported as sent until the check
223+
looked one level down.
224+
"""
225+
client = client_returning(200, {
226+
"status": "success",
227+
"data": {"status": "error", "message": "incorrect chatId format."},
228+
})
229+
with pytest.raises(FailedActionError) as excinfo:
230+
client.send_message(chat_id="bogus", message="must not count as sent")
231+
assert "did not carry the action out" in str(excinfo.value)
232+
233+
234+
def test_a_data_array_does_not_trip_the_inner_check():
235+
"""Plenty of actions return a list in `data`, which has no status at all."""
236+
client = client_returning(200, {"status": "success", "data": [{"id": 1}, {"id": 2}]})
237+
assert client.get_chats()["data"] == [{"id": 1}, {"id": 2}]

0 commit comments

Comments
 (0)