Converting BirdID's web API to FastAPI
By Tomi Chen January 19, 2021
Over the weekend, I worked on converting BirdID’s web API to FastAPI instead of Flask. While I did not notice too many performance issues or other issues with Flask itself, I was noticing some errors relating to using
asyncio.run to connect up Flask (which is synchronous) and a lot of the associated BirdID functions (since
discord.py is asynchronous).
Also, the web API has not been worked on as much as the rest of BirdID. Functions are shared between the two (as they should be ), so updates to the bot core improve the API. However, there are improvements to the web API itself that I want to make.
For this migration, the key things I wanted the new framework to have were a structure similar to Flask and native
async/await support. The two main frameworks that fit this description were Sanic and FastAPI. While Sanic was particularly appealing because it was structured so similarly to Flask, I eventually went with FastAPI because it had more features, such as data validation/typing. While the automatic docs generation is cool (check it out), it wasn’t necessary for this project.
As for performance, FastAPI’s docs say that Sanic is more comparable to Starlette since FastAPI is a higher level and adds more things on top (such as the data validation). As a result, Sanic does technically have better performance than FastAPI.
After using FastAPI, the most convenient part for me is the Starlette/FastAPI middleware for CORS, signed session cookies with itsdangerous, Sentry, and more. I’m not sure if they would work with Sanic, but the Sanic documentation did not mention anything about ASGI middleware compatibility. I would not have known these things existed anyway. While I would probably have just implemented these things myself (poorly), it was a lot easier to use the built-in stuff.
The actual process (GH#135) was not as bad as I initially expected. I worked on it through the 3 day weekend, and the bulk of the work was finished on the first day. I had a Science Olympiad competition/meetings on Saturday/Sunday, so this didn’t take that long in total.
I spent a little bit of time during the week before to skim through the docs when I was choosing which framework to use. On the first day, I went one file at a time, starting from the configuration and then converting the routes. Since I thought this was going to be a lot more difficult, I went from what I thought were the easiest files to the hardest ones. I probably overestimated the amount of code there was—it’s only like 7 files.
Anyway, it was mainly just renaming stuff to what FastAPI uses, adding the middleware, and using native
await. For example, Flask’s
blueprints are called
routers in FastAPI, Flask’s funky global objects are now passed in as a
request parameter, and CORS, gzip, and sessions are all done with ASGI middleware.
Since I was already working on the API, I also fixed some other existing issues, which were mainly with the OAuth2 Discord authentication. We use Authlib for the OAuth2 client flow to get a user’s Discord ID so scores can sync with the bot. Since May 2020, we could only use version
0.14.1 because, for some reason, upgrading would break the login flow. During the rewrite, I switched to using the Starlette/FastAPI integration for Authlib, but there were still issues.
It turns out that Authlib was storing the CSRF token inside the signed session cookie. I had
Same-Site on the cookie set to
Strict, so the cookie wasn’t being sent when Discord redirected the user back to our application. Setting
Lax fixed the issue. I’m not entirely clear on the security implications of this, but the worst thing an attacker could do is log someone out, which happens automatically after a few days anyway. Logging in isn’t that difficult either especially if you’re already logged into Discord. I would also prefer having separate signed cookies for the login flow and application session id.
I’m not sure if this is faster than Flask, especially since a lot of the functions have already been improved with better caching performance. I also don’t know if there will be fewer errors in the long term since the new version has only been up for a day or so.
According to the site analytics (we’re using Plausible with custom events), around 18 people have used it so far with about 2.1k bird checks (~26 hours after deploy), and Sentry hasn’t reported anything major.
Before deploying the new version, I tried using hey to send some load and see if FastAPI performs better. I know this probably isn’t the correct tool for the job, but hey , it “works”. However, there wasn’t too much of a difference, especially when deployed onto the server environment. I think this could either be because I’m using the wrong tool, or because there isn’t that much blocking I/O. The main blocking I/O comes from fetching media from the Macaulay Library and we heavily cache it, which is shared between the bot and web processes.
Overall, this was pretty straightforward. FastAPI is pretty easy to use, so I’ll definitely consider it for future projects. Next up: a frontend refresh—I want to stop using jQuery (sidenote: jQuery isn’t that much younger than me, like whaaa?) and use vanilla JS instead. I also want to switch to 11ty/Tailwind. After that, I’ll look into switching
sciolyid’s API to FastAPI and add the practice functionality to
sciolyid, then finally merge the BirdID and SciOly-ID sites. Fun.