AsyncTwilioHttpClient does not JSON-serialize request bodies for application/json endpoints (sync client does) → 400 on JSON-body v2 endpoints

Open Beginner friendly
#932 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
82/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
python
Domain
api

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.

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.

Dominant language
Python
Stars
2.1k
Forks
815
Avg merge
2d 13h
Merged PRs (30d)
2

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from twilio/twilio-python

All issues in twilio/twilio-python

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.