I have a FastAPI app (obvs) that has an endpoint which uses Twilio's Client to send a message sync. This has not worked. I made a bare bones python file that just constructs the client and creates the message and it works fine so I do not suspect it is twilio itself. When making a call using the twilio client the server hangs/freezes. It never times out. If I make a file change during this period the reloader freezes as well (I'm assuming since the server has become non-responsive). This happens regardless if I am using a sync or async path def for this route. Other async and sync routes seem to work fine (I haven't gotten around to testing them all yet).
fastapi==0.104.1
twilio==8.2.0
uvicorn==0.23.2
starlette==0.27.0
I am running the app locally like so (I've also called uvicorn directly from the command line):
if __name__ == '__main__':
uvicorn.run('app:app', reload=True, port=5002)
I have a router in a separate file and call app.include_router(<the_router>) in a builder function for the app. Here's the twilio client (we have our own lil wrapper):
from twilio.rest import Client
...get env variables
class TwilioAPI
def __init__(self, phone_number: str):
self.client = Client(account_sid, auth_token)
self.phone_number = phone_number
def send_sms(self, body: str):
# we enter the function, but this never returns/resolves
message = self.client.messages.create(
messaging_service_sid=messaging_service_sid,
body=body,
to=self.phone_number,
)
return message.sid
The route in question looks like this:
@router.post("/endpoint")
def send_message_or_whatever(input: Input):
...get data from input, construct message
...we hit our database here and this works
twilio_api_client = CreateAnInstanceOfOurTwilioClient()
twilio_api_client.send_sms(message) <--- this is where it goes sideways
return stuff
All the examples I have found on twilio's own blog do something like
@router.post('/endpoint')
async def do_something():
client = twilio.rest.Client() # synchronous client
client.messages.create(...create message params)
Stuff I have tried:
using async and sync path definitions. Even though we are "waiting" on twilio in a sync function it shouldn't really matter? We wait for the db at other points which is a network call with no issue. Right now I don't even care if its not the most optimal thing for performance. The client doesn't return a future that must be awaited.
when using async I have tried to use await asyncio.get_event_loop().run_in_executor(...)
to no avail, nothing happens
I tried to use fastapi's background task. It still gets stuck at client.messages.create
(I am guessing this is a wrapper around asyncio.to_thread
or run_in_executor
)
What the hell am I doing wrong?
PS: I am aware of the twilio client's AsyncHTTPClient, but I am primarily interested in understand why this doesn't work
The synchronous call is blocking the event loop. That means nothing else can occur while the blocking call is occurring. That's why you see that behavior. Running this in a thread or threadpool executor will be problematic because Twilio uses requests sessions (connection pooling) by default, which are not thread-safe.
Twilio has also made note that their client should not be considered thread-safe.
Thread safety issues can lead to problems like deadlocks. You might avoid this problem by creating your client as a threadlocal variable in the thread in which it's used, instead of reusing the same client potentially across threads.
You might also be able to turn off use of sessions/connection pooling to solve the issue (YMMV, other thread safety issues may still exist):
Ultimately, your best bet would just be to use the async client.