AsyncTwilioHttpClient does not JSON-serialize request bodies for application/json endpoints (sync client does) → 400 on JSON-body v2 endpoints
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 2.1k
- Forks
- 815
- Avg merge
- 2d 13h
- Merged PRs (30d)
- 2
Description
Environment
twilio(twilio-python): 9.10.9- Python: 3.11.15
aiohttp: 3.13.5
Description
The async HTTP client (AsyncTwilioHttpClient) is missing the Content-Type: application/json branch that the sync client has, so for any endpoint that sends a JSON request body it passes the body dict to aiohttp's data= (which form-encodes it) while still sending Content-Type: application/json. Twilio receives a body that isn't valid JSON and rejects it with 400 "Request input may be invalid".
This breaks every newer v2 JSON-body endpoint on the async client. Concrete case: client.messaging.v2.channels_senders(sid).update_async(...) (update a WhatsApp sender's business profile). The identical call on the sync client succeeds.
Root cause
Sync client serializes JSON correctly — twilio/http/http_client.py:
if headers and headers.get("Content-Type") == "application/json":
kwargs["json"] = data # real JSON body
elif headers and headers.get("Content-Type") == "application/scim+json":
kwargs["json"] = data
else:
kwargs["data"] = data
Async client has no such branch — twilio/http/async_http_client.py (request):
kwargs = {
"method": method.upper(),
"url": url,
"params": params,
"data": data, # always form-encoded, even for application/json
"headers": headers,
...
}
...
response = await session.request(**kwargs)
The generated code (e.g. messaging/v2/channels_sender.py::_update_async) correctly sets headers["Content-Type"] = "application/json" and passes data = <dict>, relying on the transport to JSON-encode — which the async client never does.
Reproduction
import asyncio
from twilio.rest import Client
from twilio.http.async_http_client import AsyncTwilioHttpClient
from twilio.rest.messaging.v2.channels_sender import ChannelsSenderList as CSL
async def main():
client = Client(SID, TOKEN, http_client=AsyncTwilioHttpClient())
body = CSL.MessagingV2ChannelsSenderRequestsUpdate(
{"profile": CSL.MessagingV2ChannelsSenderProfile({"logo_url": "https://example.com/x.jpg"})}
)
await client.messaging.v2.channels_senders("XE...").update_async(body) # -> 400
asyncio.run(main())
Result: TwilioRestException(400, '/Channels/Senders/XE...', 'Unable to update record: Request input may be invalid...').
Inspecting the captured request (http_client._test_only_last_request): data is a dict ({'profile': {'logo_url': '...'}}), not a JSON string, with Content-Type: application/json. The same update(body) on the sync TwilioHttpClient returns 200.
Expected
The async client should mirror the sync client: when Content-Type is application/json (or application/scim+json), send the body via aiohttp's json= rather than data=.
Suggested fix
In AsyncTwilioHttpClient.request, replace the unconditional "data": data with the same content-type branch the sync client uses:
if headers and headers.get("Content-Type") in ("application/json", "application/scim+json"):
kwargs["json"] = data
else:
kwargs["data"] = data
Impact
Any async call to a JSON-body endpoint fails (Channels Senders create/update, and other v2 resources that send a JSON schema body). Form-encoded endpoints (Messages, Calls, Lookups, etc.) are unaffected. Current workaround: send the request directly (e.g. httpx/aiohttp with json=), which is what the sync client does internally.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in twilio/http/async_http_client.py at AsyncTwilioHttpClient.request, then compare its request construction with twilio/http/http_client.py. Verify the async client handles application/json and application/scim+json bodies like the sync client, while form-encoded endpoints remain unaffected; reproduce with the Channels Senders update_async example and confirm the request body is valid JSON.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 82/100