fix(meta): send POST payloads in the body, not the query string

Every write went out as a query string. Graph accepts that for small
payloads, but upload_ad_image base64-encodes an entire image file, and no
real image fits in a URL — image uploads could not have worked.

POSTs now use a form body and get a longer timeout, since an upload is the
one Meta call that carries real weight. Reads and deletes keep their
parameters in the query string, where Graph expects them.

Adds the first tests over the default transport: every other Meta test
injects a fake one, so nothing covered this path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
AFFAANh
2026-08-03 17:50:43 +05:30
co-authored by Claude Opus 5
parent 4022c8a1f6
commit 584e300cd1
2 changed files with 107 additions and 2 deletions
+16 -2
View File
@@ -95,10 +95,24 @@ async def _httpx_transport(
url: str,
params: dict[str, Any],
) -> dict[str, Any]:
"""Send one Graph request.
POST payloads go in the request **body**, not the query string. Graph
accepts either, but a query string has a practical length limit of a few
kilobytes, and `upload_ad_image` base64-encodes an entire image file —
that request cannot fit in a URL for any real image.
Reads and deletes keep their parameters in the query string, which is
where Graph expects them.
"""
import httpx
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.request(method, url, params=params)
timeout = 30.0 if method != "POST" else 120.0
async with httpx.AsyncClient(timeout=timeout) as client:
if method == "POST":
response = await client.post(url, data=params)
else:
response = await client.request(method, url, params=params)
try:
return response.json()
except ValueError as exc:
+91
View File
@@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
"""The default HTTP transport. Still no live calls: httpx itself is faked.
Every other Meta test injects a transport, so nothing exercised the real
one. The property that matters here is that POST payloads go in the body:
`upload_ad_image` base64-encodes a whole image file, which cannot fit in a
query string for any real image.
"""
import httpx
import pytest
from adclaw.meta import client as meta_client
class _Recorder:
"""Stands in for httpx.AsyncClient, capturing where params landed."""
def __init__(self, calls, **kwargs):
self.calls = calls
self.init_kwargs = kwargs
async def __aenter__(self):
return self
async def __aexit__(self, *exc):
return False
def _response(self):
return httpx.Response(200, json={"id": "obj_1"})
async def post(self, url, data=None):
self.calls.append({"method": "POST", "url": url, "data": data})
return self._response()
async def request(self, method, url, params=None):
self.calls.append({"method": method, "url": url, "params": params})
return self._response()
@pytest.fixture()
def calls(monkeypatch):
recorded: list[dict] = []
monkeypatch.setattr(
httpx,
"AsyncClient",
lambda **kwargs: _Recorder(recorded, **kwargs),
)
return recorded
async def test_post_sends_its_payload_in_the_body(calls):
"""A base64 image cannot fit in a URL, so POST must not use the query."""
await meta_client._httpx_transport(
"POST", "https://graph.facebook.com/v23.0/act_1/adimages", {"bytes": "x" * 5000},
)
assert calls[0]["method"] == "POST"
assert calls[0]["data"] == {"bytes": "x" * 5000}
assert "?" not in calls[0]["url"]
async def test_get_keeps_its_params_in_the_query_string(calls):
await meta_client._httpx_transport(
"GET", "https://graph.facebook.com/v23.0/act_1", {"fields": "id"},
)
assert calls[0] == {
"method": "GET",
"url": "https://graph.facebook.com/v23.0/act_1",
"params": {"fields": "id"},
}
async def test_delete_keeps_its_params_in_the_query_string(calls):
await meta_client._httpx_transport(
"DELETE", "https://graph.facebook.com/v23.0/obj_1", {"access_token": "t"},
)
assert calls[0]["method"] == "DELETE"
assert calls[0]["params"] == {"access_token": "t"}
async def test_a_non_json_response_becomes_a_readable_error(monkeypatch):
class _Html(_Recorder):
def _response(self):
return httpx.Response(502, text="<html>bad gateway</html>")
monkeypatch.setattr(httpx, "AsyncClient", lambda **kwargs: _Html([], **kwargs))
with pytest.raises(meta_client.MetaError, match="non-JSON"):
await meta_client._httpx_transport("GET", "https://graph.facebook.com/x", {})