Jonxslays / unkey.py

An asynchronous Python SDK for unkey.dev.
https://jonxslays.github.io/unkey.py/
GNU General Public License v3.0
7 stars 1 forks source link

Python FastAPI concurrency `ServerDisconnectedError` with protected decorator #13

Closed fungs closed 5 months ago

fungs commented 5 months ago

Using the Python API with FastAPI and a decorator that protects a resource with an API key, as written in the latest docs, I'm getting exceptions when requests are called concurrently (as usual for an API).

I'd expect the application not to crash and the verification decorator not to fail. Instead, when using the proposed decorator, there should be an internal handling of request concurrency errors and some kind of cache and retry logic to handle such request bursts.

I have seen the on_exc callback handler, but I assume it does not handle this scenario, right?

Error:

INFO:     172.17.0.17924 - "GET / HTTP/1.1" 500 Internal Server Error ERROR:    Exception in ASGI application Traceback (most recent call last):   File "/o/installpath/lib/python3.12/site-packages-datapipe-api-env/lib/python3.12/site-packages/uvicorn/protocols/http/httptools_impl.py", line 419, in run_asgio    result = await app(  # type: ignore[func-returns-value]t             ^^^^^^^^^/installpath/lib/python3.12/site-packages^^^^^^^^^^^^^^^^^^^^^^^^   File "/installpath/lib/python3.12/site-packages/uvicorn/middapi-env/lib/python3.12/site-packagesheaders.py", line 84, in __call__o    return await self.app(scope, receive, send)api-env/lib/python3.12/site-packages/uvicorn/middleware/proxy_headers.py", line 84, in __call__
    return await self.app(scope, receive, send)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/fastapi/applications.py", line 1054, in __call__
    await super().__call__(scope, receive, send)
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/starlette/applications.py", line 123, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/starlette/middleware/errors.py", line 186, in __call__
    raise exc
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/starlette/middleware/errors.py", line 164, in __call__
    await self.app(scope, receive, _send)
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/starlette/middleware/exceptions.py", line 62, in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
    raise exc
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    await app(scope, receive, sender)
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/starlette/routing.py", line 758, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/starlette/routing.py", line 778, in app
    await route.handle(scope, receive, send)
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/starlette/routing.py", line 299, in handle
    await self.app(scope, receive, send)
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/starlette/routing.py", line 79, in app
    await wrap_app_handling_exceptions(app, request)(scope, receive, send)
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
    raise exc
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    await app(scope, receive, sender)
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/starlette/routing.py", line 74, in app
    response = await func(request)
               ^^^^^^^^^^^^^^^^^^^
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/fastapi/routing.py", line 278, in app
    raw_response = await run_endpoint_function(
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/fastapi/routing.py", line 191, in run_endpoint_function
    return await dependant.call(**values)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/unkey/decorators.py", line 135, in inner
    return _on_exc(exc)
           ^^^^^^^^^^^^
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/unkey/decorators.py", line 99, in _on_exc
    raise exc
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/unkey/decorators.py", line 112, in inner
    result = await _client.keys.verify_key(key, api_id)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/unkey/services/keys.py", line 120, in verify_key
    data = await self._http.fetch(route, payload=payload)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/unkey/services/http.py", line 146, in fetch
    return await self._request(  # type: ignore[no-any-return]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/unkey/services/http.py", line 70, in _request
    response = await req(url, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/aiohttp/client.py", line 605, in _request
    await resp.start(conn)
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/aiohttp/client_reqrep.py", line 966, in start
    message, payload = await protocol.read()  # type: ignore[union-attr]
                       ^^^^^^^^^^^^^^^^^^^^^
  File "/installpath/lib/python3.12/site-packages/lib/python3.12/site-packages/aiohttp/streams.py", line 622, in read
    await self._waiter
aiohttp.client_exceptions.ServerDisconnectedError: Server disconnected
  1. protect a route with a decorator
  2. launch say 10 parallel curl commands that request the same resource

sample (python) FastAPI code:

import typing
import unkey
import fastapi

UNKEY_API_ID='your_api_key'

def api_key_extractor(*args: typing.Any, **kwargs: typing.Any) -> typing.Optional[str]:
    """Extract the API key from the Bearer token header in request, used by unkey.protected() decorator."""
    if isinstance(auth := kwargs.get("authorization"), str):
        api_key = auth.split(" ")[-1]
        return api_key
    return None

@app.get("/")
@unkey.protected(UNKEY_API_ID, api_key_extractor)
async def reroute_broker(
        *,
        authorization: str = fastapi.Header(None),
        unkey_verification: typing.Any = None,
    ) -> fastapi.Response:
    """Sample route"""

    assert isinstance(unkey_verification, unkey.ApiKeyVerification)
    assert unkey_verification.valid
    print(unkey_verification.owner_id)
    return {"message": "protected!"}

sample (sh) request code:

BASE_URL='your_url'
AUTH_TOKEN='api_token'

for i in $(seq 1 10); do
    curl -H "Authorization: Bearer $AUTH_TOKEN" "$BASE_URL" &
done

wait

Python 3.12.2 using the following packages:

Package                 Version  Editable project location
----------------------- -------- -----------------------------------
aiohttp                 3.9.3
aiosignal               1.3.1
annotated-types         0.6.0
anyio                   4.3.0
dnspython               2.6.1
fastapi                 0.110.0
gunicorn                21.2.0
httpcore                1.0.4
httptools               0.6.1
httpx                   0.27.0
humanfriendly           10.0
idna                    3.6
itsdangerous            2.1.2
sniffio                 1.3.1
starlette               0.36.3
tomli                   2.0.1
typing_extensions       4.10.0
unkey.py                0.7.1
uvicorn                 0.27.1
uvloop                  0.19.0
websockets              12.0
Jonxslays commented 5 months ago

Very interesting, thanks a lot for the detailed report. I will investigate this evening after work.

Jonxslays commented 5 months ago

This has been resolved in v0.7.2, thanks again!

fungs commented 5 months ago

grafik