GithubHelp home page GithubHelp logo

tomwojcik / starlette-context Goto Github PK

View Code? Open in Web Editor NEW
419.0 5.0 23.0 417 KB

Middleware for Starlette that allows you to store and access the context data of a request. Can be used with logging so logs automatically use request headers such as x-request-id or x-correlation-id.

Home Page: https://starlette-context.readthedocs.io/en/latest/

License: MIT License

Python 96.21% Shell 2.33% Makefile 1.45%
python python3 starlette fastapi starlette-context middleware

starlette-context's People

Contributors

adamantike avatar dependabot[bot] avatar ginomempin avatar hhamana avatar kirillzhosul avatar tomwojcik avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

starlette-context's Issues

How to get request.body from starlette-context plugin?

I am using fastapi to build website and I want to get request.body() for logging. I import starlette-context==0.3.3 to get a global context from request.

Since the default plugin could only get variable from request.header, I need to write a plugin to get request.body()

I could easily get some variable like request.url, but I When I write plugin to get my request body, I got problem 'RuntimeWarning: coroutine 'Request.body' was never awaited' and my route of fastapi could not get any request.

My plugin code is :

from typing import Any, Optional, Union

from starlette.requests import HTTPConnection, Request
from starlette_context.plugins import Plugin

class BodyPlugin(Plugin):
    key = "body"

    async def process_request(
        self, request: Union[Request, HTTPConnection]
    ) -> Optional[Any]:
        body = await request.body()

        return body

And my middleware is below:

middleware = [
    Middleware(
        ContextMiddleware,
        plugins=(
            BodyPlugin()
        )
    )
]

Possible to have multiple RawContextMiddlewares?

First off, thank you for your hard work on this package!

We use this in a few places in our ecosystem, and now we are in a situation where we likely will need a few different plugins in the context middleware. Our current implementation is a private internal package which subclasses the middleware and allows internal fastapi consumers to add it to their stack. All works well, we have had no issues with this approach.

Now we are looking to add a second RawContextMiddleware subclass in a different package, and are looking to take the same approach. But when we went to implement it, we found that only the first middleware added would work, and the second would not.

The engineer working on this got around it by pulling the plugin from one package and adding to the context middleware of the other, but this tightly couples them in ways we wish not to.

The expectation is that we should be able to do

app.add_middleware(SubclassedRawContextMiddleware1)
app.add_middleware(SubclassedRawContextMiddleware2)

and have both apply, chained, like other middlewares. Is there something in the codebase that prevents this expectation from happening?

Exceptions should inherit from Exception, not BaseException

It seems to me like StarletteContextError in errors.py should subclass Exception instead of BaseException.

Exceptions that inherit from BaseException are not caught by the normal try/catch idiom of:

try:
    block_of_code()
exception Exception as e:
    handle(e)

BaseException is meant for exception types that shouldn't be caught by normal handlers (SystemInterrupt, etc).

If this was an intentional decision on your part, that's fine, but I didn't see any issues or discussion about it. I'd be happy to submit a PR for this if you like.

I love this package and I use it in a few of my FastAPI projects!

Can't use http middleware

I tried using starlette-context and it solves most of my problems as a near to perfect equivalent for Flask g.
Now I am facing issue in setting the request_id in the FastAPI's "http" middleware, my main.py is as follows:

import uuid

from fastapi import FastAPI, Request
from fastapi.encoders import jsonable_encoder
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from starlette_context import context
from starlette_context.middleware import RawContextMiddleware

from app.api.auth import security_router
from app.api.health import status_router
from app.api.v1.router import v1_router
from app.core.config import settings
from app.database import Base, engine
from app.exceptions import CustomException


def get_application():
    _app = FastAPI(title="referral-service")
    allowed_origins = [str(origin) for origin in settings.BACKEND_CORS_ORIGINS]

    _app.add_middleware(
        CORSMiddleware,
        allow_origins=allowed_origins,
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )
    _app.add_middleware(RawContextMiddleware)

    _app.include_router(v1_router, prefix="/api/v1")
    _app.include_router(security_router)
    _app.include_router(status_router)

    return _app


app = get_application()


@app.on_event("startup")
async def startup():
    # create db tables
    async with engine.begin() as conn:
        # await conn.run_sync(Base.metadata.drop_all)
        await conn.run_sync(Base.metadata.create_all)


@app.middleware("http")
async def set_request_context(request: Request, call_next):
    request_id = uuid.uuid4()
    context["request_id"] = request_id
    response = await call_next(request)
    return response


@app.exception_handler(CustomException)
async def custom_exception_handler(request: Request, exc: CustomException):
    return JSONResponse(
        jsonable_encoder(exc.response), status_code=exc.status_code
    )

When i run any of my route, it sends me the following error:

Traceback (most recent call last):
  File "/Users/rahulagarwal/Library/Caches/pypoetry/virtualenvs/referral-service-bzkw0PDN-py3.9/lib/python3.9/site-packages/uvicorn/protocols/http/h11_impl.py", line 366, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "/Users/rahulagarwal/Library/Caches/pypoetry/virtualenvs/referral-service-bzkw0PDN-py3.9/lib/python3.9/site-packages/uvicorn/middleware/proxy_headers.py", line 75, in __call__
    return await self.app(scope, receive, send)
  File "/Users/rahulagarwal/Library/Caches/pypoetry/virtualenvs/referral-service-bzkw0PDN-py3.9/lib/python3.9/site-packages/fastapi/applications.py", line 261, in __call__
    await super().__call__(scope, receive, send)
  File "/Users/rahulagarwal/Library/Caches/pypoetry/virtualenvs/referral-service-bzkw0PDN-py3.9/lib/python3.9/site-packages/starlette/applications.py", line 112, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/Users/rahulagarwal/Library/Caches/pypoetry/virtualenvs/referral-service-bzkw0PDN-py3.9/lib/python3.9/site-packages/starlette/middleware/errors.py", line 181, in __call__
    raise exc
  File "/Users/rahulagarwal/Library/Caches/pypoetry/virtualenvs/referral-service-bzkw0PDN-py3.9/lib/python3.9/site-packages/starlette/middleware/errors.py", line 159, in __call__
    await self.app(scope, receive, _send)
  File "/Users/rahulagarwal/Library/Caches/pypoetry/virtualenvs/referral-service-bzkw0PDN-py3.9/lib/python3.9/site-packages/starlette/middleware/base.py", line 63, in __call__
    response = await self.dispatch_func(request, call_next)
  File "/Users/rahulagarwal/savii-code/referral-service/./app/main.py", line 58, in set_request_context
    context["request_id"] = request_id
  File "/opt/homebrew/Cellar/[email protected]/3.9.12/Frameworks/Python.framework/Versions/3.9/lib/python3.9/collections/__init__.py", line 1061, in __setitem__
    self.data[key] = item
  File "/Users/rahulagarwal/Library/Caches/pypoetry/virtualenvs/referral-service-bzkw0PDN-py3.9/lib/python3.9/site-packages/starlette_context/ctx.py", line 35, in data
    raise ContextDoesNotExistError
starlette_context.errors.ContextDoesNotExistError: You didn't use the required middleware or you're trying to access `context` object outside of the request-response cycle.

Am i doing something wrong with this? As far as I understand the middleware is a part of request-response cycle, so why am I getting the error that this is not part of the request-response cycle. Please help

Can't write new plugin - plugin is not iterable

Hi!

I am quite new to Starlette and FastAPI, and I might have holes in my knowledge, however I was not able to write a new plugin.

from fastapi import FastAPI
from fastapi.middleware import Middleware

from starlette_context.header_keys import HeaderKeys
from starlette_context.plugins import Plugin

class LangPlugin(Plugin):
    print('****')

middleware = [
    Middleware(
        RawContextMiddleware,
        plugins(
            LangPlugin()
        )
    )
]

app = FastAPI(middleware=middleware)

And it says:

  File ".../starlette_context/middleware/raw_middleware.py", line 17, in __init__
    if not all([isinstance(plugin, Plugin) for plugin in self.plugins]):
TypeError: 'LangPlugin' object is not iterable

How is my plugin not instance of Plugin, I wonder. Or I guess this is the problem.

To put you into context, I would like to get from here an optional string param, (/path-to-api-endpoint/?lang=en) to access the request language from everywhere. Or maybe you have a more straightforward solution to my problem, what I did not recognize?

JWT token?

Is there a way to access the authorization header to capture the JWT token?

No module named '_contextvars'

Hello,

First of all thanks for your contribution :D

Sorry as I reach you due to a bug highlighted in the unittest.
Any idea on it for a quick fix?
Thanks in advance.

14:54:23  ======================================================================
14:54:23  ERROR: Failure: ModuleNotFoundError (No module named '_contextvars')
14:54:23  ----------------------------------------------------------------------
14:54:23  Traceback (most recent call last):
14:54:23    File ".venv/lib64/python3.6/site-packages/nose/failure.py", line 39, in runTest
14:54:23      raise self.exc_val.with_traceback(self.tb)
14:54:23    File ".venv/lib64/python3.6/site-packages/nose/loader.py", line 418, in loadTestsFromName
14:54:23      addr.filename, addr.module)
14:54:23    File ".venv/lib64/python3.6/site-packages/nose/importer.py", line 47, in importFromPath
14:54:23      return self.importFromDir(dir_path, fqname)
14:54:23    File ".venv/lib64/python3.6/site-packages/nose/importer.py", line 94, in importFromDir
14:54:23      mod = load_module(part_fqname, fh, filename, desc)
14:54:23    File "/usr/lib64/python3.6/imp.py", line 235, in load_module
14:54:23      return load_source(name, filename, file)
14:54:23    File "/usr/lib64/python3.6/imp.py", line 172, in load_source
14:54:23      module = _load(spec)
14:54:23    File "<frozen importlib._bootstrap>", line 684, in _load
14:54:23    File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
14:54:23    File "<frozen importlib._bootstrap_external>", line 678, in exec_module
14:54:23    File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
14:54:23    File "test/test_Tracking.py", line 11, in <module>
14:54:23      from main import app
14:54:23    File "test/../app/main.py", line 10, in <module>
14:54:23      from lib.middleware import MIDDLEWARES
14:54:23    File "test/../app/lib/middleware.py", line 2, in <module>
14:54:23      from starlette_context import plugins
14:54:23    File ".venv/lib64/python3.6/site-packages/starlette_context/__init__.py", line 10, in <module>
14:54:23      from starlette_context.middlewares.basic_context_middleware import (  # noqa
14:54:23    File ".venv/lib64/python3.6/site-packages/starlette_context/middlewares/basic_context_middleware.py", line 1, in <module>
14:54:23      from _contextvars import Token
14:54:23  ModuleNotFoundError: No module named '_contextvars'

Access context from another middleware

Hi, thanks for this great middleware! ๐Ÿ˜„
I was trying to access context from another middleware and was getting a lot of RuntimeErrors saying I was not in request-response cycle.

Problem is solved, I am leaving this issue in case somebody else have this "access context in another middleware" use-case and is getting RuntimeErrors. You should add RawContextMiddleware last (see: encode/starlette#479):

app = Starlette()  # or FastAPI()

# Add your middleware that uses context here...

app.add_middleware(RawContextMiddleware)

It would be great if you could add a warning about this in docs' examples, but I don't insist as it is actually more of a Starlette problem

Consider removing `RawContextMiddleware` and keeping only `ContextMiddleware` after resolving memory usage issue

I like ContextMiddleware because it's very simple to understand and expand, even for minds that are not familiar with async Python.

Some time ago it was advised #18 to add RawContextMiddleware and Starlette maintainers were discouraging the use of the built-in middleware. I was surprised to see 3 โค๏ธ reactions under this issue so I think a few people had problems with it.

Fast-forward almost a year, the issue encode/starlette#1012 (comment) with memory usage has been closed.

If there's no point in keeping both, I'd be happy to remove the more complicated one but I will wait for some confirmations from the community. If there's a case when it makes sense to use RawContextMiddleware, then I'd keep both.

What's your opinion about that? @hhamana @dmig-alarstudios

Properly use pyproject.toml

The pyproject.toml we use is currently only for black config.
While that's fine and all, pyproject.toml is originally intended to define the build system, and replace setup.cfg, which itself is already supposed to be a safer alternative to the setup.py
As build system, using the status quo default setuptools is fine for us. But we have to be explicit about it. Latest updates to pip issue a DeprecatedWarning to systems still using setup.py or setup.cfg to define project metadata.

recommended reading:

Access content in thread

Hi, thank you for this project.

I use the application context to log authorization data. It works fine, until an application start a thread. Is it possible to use the context in threads, started while a request?

Thank you.

Intended behavior on BackgroundTasks

I have fairly recently found a point of confusion, that I cannot explain to myself.
Starlette (and FastApi by extension) provide a BackgroundTask feature, which allows one to do further processing after a request has been sent.
I was expecting this to be incompatible with the context provided by starlette_context, as the middleware explicitly resets the context before sending the response, leading to the You didn't use the required middleware or you're trying to access `context` object outside of the request-response cycle. error. The background task is ran after the response, therefore is out the that cycle.
However, this is not what happens. The context can be accessed within a background task. It does not error, and can access the appropriate context data from the request that spawned it.
The response is returned with correct headers, indicating the middleware does indeed process the response, and the background task is very much processed after that. I haven't seen any confusion of data between concurrent requests either.
It's all good, but I don't understand how.

Python 3.6 installs 0.1.3 version

Seems like new releases are no longer publish for python 3.6 (even with contextvars library). This should probably be noted in the README?

On python 3.6.6 (and 0.1.3) I faced error:

...
  File "/home/musttu/.pyenv/versions/3.6.6/envs/famp/lib/python3.6/site-packages/starlette_context/middlewares/basic_context_middleware.py", line 1, in <module>
    from _contextvars import Token
ModuleNotFoundError: No module named '_contextvars'

How to use context in Jinja (or any other template engine)?

How can i use my created context in template globally?
I know that we can do it by adding context in each @app.rout separately but I want to show context in base template and it is really meaningless to add context for each route for this purpose.

0.3.3 does not contain the testing helper request_cycle_context

Hey there thanks for this great app!

Small problem with latest release vs docs vs testing

installing 0.3.3 with extensive tests but the request_cycle_context is not present in that release but is in documentation and is needed to write useful tests.

installing from HEAD to get around this issue.

Add request-id to logs without using LoggerAdapter

Thank you for this middleware!

I want to use it for adding request IDs in the logs of third-party libraries that I do not have control over. Since most libraries might use a logger instead of LoggerAdapter, I am wondering if there is a way to log request IDs without the use LoggerAdapter(like in the example given in this repo).

RequestTime plugin support.

I have develop a web system base on fastapi which use starlette as a basic framework. I wrote a plugin to calculate how much time an request take, here is the code:

import time
from typing import Union

from starlette.requests import Request
from starlette.responses import Response

from starlette_context.plugins.plugin import Plugin
from starlette_context import _request_scope_context_storage


class RequestTimePlugin(Plugin):
    key = "X-Request-Time"

    async def process_request(self, request: Request) -> Union[str, int, dict, float]:
        """
        Runs always on request.
        Extracts value from header by default.
        """
        assert isinstance(self.key, str)
        return time.time()

    async def enrich_response(self, response: Response) -> None:
        context: dict = _request_scope_context_storage.get()
        start_time = context.get(self.key)
        context[self.key] = time.time() - start_time
        _request_scope_context_storage.set(context)

the problem is i want request-id and request-time can be logging inside request_access log,but when the access log is print ,the whole dispatch function is finished.I notice you have reset the contextvar at the bottom of dispatch function.So the code for access log:

class RequestAccessFormatter(AccessFormatter):
    def __init__(self, fmt=None, datefmt=None, style="%", use_colors=None):
        super().__init__(fmt=fmt, datefmt=datefmt, style=style)
        self.use_colors = False

    def get_status_code(self, record):
        status_code = record.__dict__["status_code"]
        return status_code

    def get_request_time(self):
        return round(context.data['X-Request-Time'], 3)

    def get_request_id(self):
        return context.data['X-Request-ID']

    def get_user_agent(self, scope):
        user_agent = '-'
        for header_name, header_value in scope.get("headers", []):
            if 'user-agent' in str(header_name):
                user_agent = header_value
                break
        return user_agent

    def formatMessage(self, record):
        record_copy = copy(record)
        # user_agent = self.get_user_agent(scope)
        request_time = self.get_request_time()
        request_id = self.get_request_id()

        # record_copy.__dict__['user_agent'] = user_agent
        record_copy.__dict__['request_time'] = request_time
        record_copy.__dict__['request_id'] = request_id
        return super().formatMessage(record_copy)

will get the error:

  File "D:\py_workspace\demo\common\log_handler.py", line 56, in formatMessage
    request_time = self.get_request_time()
  File "D:\py_workspace\demo\common\log_handler.py", line 40, in get_request_time
    return context.data['X-Request-Time']
  File "D:\py_workspace\demo\venv\lib\site-packages\starlette_context\ctx.py", line 28, in data
    raise RuntimeError(
RuntimeError: You didn't use ContextMiddleware or you're trying to access `context` object outside of the request-response cycle.

I have two solution for this, one is not reset contextvar at bottom of dispatch function,the other way is copy contextvar to request.scope, which one is better? I you think one of this way is ok, i will pull a PR to support RequestTime.

Access log format will be something like:

ACCESS_LOG_FORMAT = '[%(asctime)s] %(request_id)s %(status_code)s %(request_time)ss %(client_addr)s ' \
                    '- "%(request_line)s"'

[2020-09-30 11:45:28,769] 678bba8a5a754d41928135b468171ddc 200 0.008s 172.37.2.55:49921 - "PUT /api/xxx/xxxx/xxxx HTTP/1.1"

Test fails due to an uninitialized context

I have a FastAPI app, that is the same used in the py.test unit tests. Also, RawContextMiddleware is the only middleware I have, coming from startlette_context, of course.

Responsibility of the middleware is to read the value of a custom http header, and put something in the context object. It works perfectly if I test the app, but when I run tests (which were already in place and successfully executed before I added startlette_middleware) a couple break, with the following:

You didn't use ContextMiddleware or you're trying to access context object outside of the request-response cycle

Maybe starlette TestClient requires some specific setup to deal with middlewares ?

Provide option in RequestIdPlugin/CorrelationIdPlugin to always create a new UUID

Instead of accepting the user provided id, it would be nice if there was a way to always use the server-side generated id.

Currently, if the client has set the header to a value, that value is always preferred so the server doesn't generate a new one. It would be nice if both modes could be supported through a new kwarg which wouldn't introduce a breaking change.

async def extract_value_from_header_by_key(
self, request: Request
) -> Optional[str]:
await super(
CorrelationIdPlugin, self
).extract_value_from_header_by_key(request)
if self.value is None:
self.value = uuid.uuid4().hex
return self.value

Happy to submit a PR if needed.

Question: Can I add request-id into uvicorn.access log?

My log now is:

[2021-04-25 08:48:58 +0000] [13] [INFO] RID: ad303ddacea942b89c6f08e33b5f09be | CID: d68deb856ea84d4e8e7bc7c08bc511bd | s.m.users.business.controllers | An user is trying to log in our system

[2021-04-25 08:48:58 +0000] [13] [INFO] uvicorn.access | 172.21.0.1:64268 - "POST /v1/users/login HTTP/1.1" 200

I tried to add filter to log request-id and correlation-id to uvicorn.access but it raised starlette_context.errors.ContextDoesNotExistError.

Is there a way to add context data to uvicorn.access log?

async pytests fail when using logging middleware from the examples

Hi, I just added asnyc tests following the fastapi documentation to my app.

When I am using the logging middleware as implemented in the example, it throws errors.

Pytest Output
wolf-python-app on ๎‚  wip/dev [!?] via ๐Ÿ v3.9.7 (wolf-python-app) โฏ pytest tests/
=========================================================================================== test session starts ============================================================================================
platform linux -- Python 3.9.7, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /wolf-python-app
plugins: anyio-3.3.2
collected 2 items                                                                                                                                                                                          

tests/test_main.py FF                                                                                                                                                                                [100%]

================================================================================================= FAILURES =================================================================================================
____________________________________________________________________________________________ test_root[asyncio] ____________________________________________________________________________________________

    @pytest.mark.anyio
    async def test_root() -> None:
        async with AsyncClient(app=create_app(), base_url="http://test") as ac:
>           response = await ac.get("/")

tests/test_main.py:10: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.9/site-packages/httpx/_client.py:1704: in get
    return await self.request(
.venv/lib/python3.9/site-packages/httpx/_client.py:1483: in request
    response = await self.send(
.venv/lib/python3.9/site-packages/httpx/_client.py:1571: in send
    response = await self._send_handling_auth(
.venv/lib/python3.9/site-packages/httpx/_client.py:1599: in _send_handling_auth
    response = await self._send_handling_redirects(
.venv/lib/python3.9/site-packages/httpx/_client.py:1636: in _send_handling_redirects
    response = await self._send_single_request(request)
.venv/lib/python3.9/site-packages/httpx/_client.py:1673: in _send_single_request
    response = await transport.handle_async_request(request)
.venv/lib/python3.9/site-packages/httpx/_transports/asgi.py:152: in handle_async_request
    await self.app(scope, receive, send)
.venv/lib/python3.9/site-packages/fastapi/applications.py:208: in __call__
    await super().__call__(scope, receive, send)
.venv/lib/python3.9/site-packages/starlette/applications.py:112: in __call__
    await self.middleware_stack(scope, receive, send)
.venv/lib/python3.9/site-packages/starlette/middleware/errors.py:181: in __call__
    raise exc from None
.venv/lib/python3.9/site-packages/starlette/middleware/errors.py:159: in __call__
    await self.app(scope, receive, _send)
.venv/lib/python3.9/site-packages/starlette_context/middleware/raw_middleware.py:96: in __call__
    await self.app(scope, receive, send_wrapper)
.venv/lib/python3.9/site-packages/starlette/middleware/base.py:25: in __call__
    response = await self.dispatch_func(request, self.call_next)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <wolf.my_namespace.my_package.core.middleware.LoggingMiddleware object at 0x7fd71a53eca0>, request = <starlette.requests.Request object at 0x7fd71a4e86d0>
call_next = <bound method BaseHTTPMiddleware.call_next of <wolf.my_namespace.my_package.core.middleware.LoggingMiddleware object at 0x7fd71a53eca0>>

    async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
>       await logger.info("http request", method=request.method, path=request.url.path)
E       TypeError: object NoneType can't be used in 'await' expression

wolf/my_namespace/my_package/core/middleware.py:10: TypeError
------------------------------------------------------------------------------------------- Captured stdout call -------------------------------------------------------------------------------------------
2021-10-07 06:23.37 [info     ] http request                   method=GET path=/
_____________________________________________________________________________________________ test_root[trio] ______________________________________________________________________________________________

    @pytest.mark.anyio
    async def test_root() -> None:
        async with AsyncClient(app=create_app(), base_url="http://test") as ac:
>           response = await ac.get("/")

tests/test_main.py:10: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.9/site-packages/httpx/_client.py:1704: in get
    return await self.request(
.venv/lib/python3.9/site-packages/httpx/_client.py:1483: in request
    response = await self.send(
.venv/lib/python3.9/site-packages/httpx/_client.py:1571: in send
    response = await self._send_handling_auth(
.venv/lib/python3.9/site-packages/httpx/_client.py:1599: in _send_handling_auth
    response = await self._send_handling_redirects(
.venv/lib/python3.9/site-packages/httpx/_client.py:1636: in _send_handling_redirects
    response = await self._send_single_request(request)
.venv/lib/python3.9/site-packages/httpx/_client.py:1673: in _send_single_request
    response = await transport.handle_async_request(request)
.venv/lib/python3.9/site-packages/httpx/_transports/asgi.py:152: in handle_async_request
    await self.app(scope, receive, send)
.venv/lib/python3.9/site-packages/fastapi/applications.py:208: in __call__
    await super().__call__(scope, receive, send)
.venv/lib/python3.9/site-packages/starlette/applications.py:112: in __call__
    await self.middleware_stack(scope, receive, send)
.venv/lib/python3.9/site-packages/starlette/middleware/errors.py:181: in __call__
    raise exc from None
.venv/lib/python3.9/site-packages/starlette/middleware/errors.py:159: in __call__
    await self.app(scope, receive, _send)
.venv/lib/python3.9/site-packages/starlette_context/middleware/raw_middleware.py:96: in __call__
    await self.app(scope, receive, send_wrapper)
.venv/lib/python3.9/site-packages/starlette/middleware/base.py:25: in __call__
    response = await self.dispatch_func(request, self.call_next)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <wolf.my_namespace.my_package.core.middleware.LoggingMiddleware object at 0x7fd71a26d460>, request = <starlette.requests.Request object at 0x7fd71a242d60>
call_next = <bound method BaseHTTPMiddleware.call_next of <wolf.my_namespace.my_package.core.middleware.LoggingMiddleware object at 0x7fd71a26d460>>

    async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
>       await logger.info("http request", method=request.method, path=request.url.path)
E       TypeError: object NoneType can't be used in 'await' expression

wolf/my_namespace/my_package/core/middleware.py:10: TypeError
------------------------------------------------------------------------------------------- Captured stdout call -------------------------------------------------------------------------------------------
2021-10-07 06:23.38 [info     ] http request                   method=GET path=/
============================================================================================= warnings summary =============================================================================================
tests/test_main.py::test_root[asyncio]
  /usr/local/lib/python3.9/asyncio/base_events.py:542: DeprecationWarning: The loop argument is deprecated since Python 3.8, and scheduled for removal in Python 3.10.
    results = await tasks.gather(

-- Docs: https://docs.pytest.org/en/stable/warnings.html
========================================================================================= short test summary info ==========================================================================================
FAILED tests/test_main.py::test_root[asyncio] - TypeError: object NoneType can't be used in 'await' expression
FAILED tests/test_main.py::test_root[trio] - TypeError: object NoneType can't be used in 'await' expression
======================================================================================= 2 failed, 1 warning in 0.67s =======================================================================================

I have implemented the middleware the same way as shown in the example, and it works when running the app normally.

from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from structlog import get_logger

logger = get_logger()


class LoggingMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
        await logger.info("http request", method=request.method, path=request.url.path)
        response = await call_next(request)
        await logger.info("http response", statusCode=response.status_code)
        return response
from fastapi import FastAPI
from starlette_context.middleware import RawContextMiddleware  # type: ignore[attr-defined]
from starlette_context.plugins import CorrelationIdPlugin, RequestIdPlugin  # type: ignore[attr-defined]

from wolf.my_namespace.my_package.core import config, middleware, tasks
from wolf.my_namespace.my_package.routers.index import base


def create_app() -> FastAPI:
    app = FastAPI(title=config.PROJECT_NAME, version=config.VERSION)
    app.add_event_handler("startup", tasks.create_start_app_handler())
    app.add_event_handler("shutdown", tasks.create_stop_app_handler())
    app.add_middleware(middleware.LoggingMiddleware)
    app.add_middleware(
        RawContextMiddleware,
        plugins=(RequestIdPlugin(), CorrelationIdPlugin()),
    )
    app.include_router(base)
    return app

When I comment out the await calles to the structlog logger. I get a different error.

Pytest Output
wolf-python-app on ๎‚  wip/dev [!?] via ๐Ÿ v3.9.7 (wolf-python-app) โฏ pytest tests/
=========================================================================================== test session starts ============================================================================================
platform linux -- Python 3.9.7, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /wolf-python-app
plugins: anyio-3.3.2
collected 2 items                                                                                                                                                                                          

tests/test_main.py .F                                                                                                                                                                                [100%]

================================================================================================= FAILURES =================================================================================================
_____________________________________________________________________________________________ test_root[trio] ______________________________________________________________________________________________

    @pytest.mark.anyio
    async def test_root() -> None:
        async with AsyncClient(app=create_app(), base_url="http://test") as ac:
>           response = await ac.get("/")

tests/test_main.py:10: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.9/site-packages/httpx/_client.py:1704: in get
    return await self.request(
.venv/lib/python3.9/site-packages/httpx/_client.py:1483: in request
    response = await self.send(
.venv/lib/python3.9/site-packages/httpx/_client.py:1571: in send
    response = await self._send_handling_auth(
.venv/lib/python3.9/site-packages/httpx/_client.py:1599: in _send_handling_auth
    response = await self._send_handling_redirects(
.venv/lib/python3.9/site-packages/httpx/_client.py:1636: in _send_handling_redirects
    response = await self._send_single_request(request)
.venv/lib/python3.9/site-packages/httpx/_client.py:1673: in _send_single_request
    response = await transport.handle_async_request(request)
.venv/lib/python3.9/site-packages/httpx/_transports/asgi.py:152: in handle_async_request
    await self.app(scope, receive, send)
.venv/lib/python3.9/site-packages/fastapi/applications.py:208: in __call__
    await super().__call__(scope, receive, send)
.venv/lib/python3.9/site-packages/starlette/applications.py:112: in __call__
    await self.middleware_stack(scope, receive, send)
.venv/lib/python3.9/site-packages/starlette/middleware/errors.py:181: in __call__
    raise exc from None
.venv/lib/python3.9/site-packages/starlette/middleware/errors.py:159: in __call__
    await self.app(scope, receive, _send)
.venv/lib/python3.9/site-packages/starlette_context/middleware/raw_middleware.py:96: in __call__
    await self.app(scope, receive, send_wrapper)
.venv/lib/python3.9/site-packages/starlette/middleware/base.py:25: in __call__
    response = await self.dispatch_func(request, self.call_next)
wolf/my_namespace/my_package/core/middleware.py:11: in dispatch
    response = await call_next(request)
.venv/lib/python3.9/site-packages/starlette/middleware/base.py:29: in call_next
    loop = asyncio.get_event_loop()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <asyncio.unix_events._UnixDefaultEventLoopPolicy object at 0x7fe2f6344a60>

    def get_event_loop(self):
        """Get the event loop for the current context.
    
        Returns an instance of EventLoop or raises an exception.
        """
        if (self._local._loop is None and
                not self._local._set_called and
                threading.current_thread() is threading.main_thread()):
            self.set_event_loop(self.new_event_loop())
    
        if self._local._loop is None:
>           raise RuntimeError('There is no current event loop in thread %r.'
                               % threading.current_thread().name)
E           RuntimeError: There is no current event loop in thread 'MainThread'.

/usr/local/lib/python3.9/asyncio/events.py:642: RuntimeError
========================================================================================= short test summary info ==========================================================================================
FAILED tests/test_main.py::test_root[trio] - RuntimeError: There is no current event loop in thread 'MainThread'.
======================================================================================= 1 failed, 1 passed in 0.59s ========================================================================================

When I comment out using the loggingMiddleware, the test passes.

#  app.add_middleware(middleware.LoggingMiddleware)

ContextDoesNotExistError on FastAPI Custom Exception Handler

Hi,
i'm using Depend FastAPI mechaninsm to access Context on request-response cycle:

async def my_context_dependency(
    x_plt_session_id: str = Header(None),
    x_plt_correlation_id: str = Header(None),
    x_plt_user_id: str = Header(None),
    x_plt_event_id: str = Header(None),
    x_plt_solution_user: str = Header(None),
) -> Any:
    # When used a Depends(), this fucntion get the `X-Client_ID` header,
    # which will be documented as a required header by FastAPI.
    # use `x_client_id: str = Header(None)` for an optional header.

    data = {
        "session-id": x_plt_session_id,
        "correlation-id": x_plt_correlation_id,
        "user-id": x_plt_user_id,
        "event-id": x_plt_event_id,
        "solution-user": x_plt_solution_user,
    }
    with request_cycle_context(data):
        # yield allows it to pass along to the rest of the request
        yield

app = FastAPI(
        dependencies=[Depends(my_context_dependency)],
        title="Virtual Entity API",
        description="This is a very fancy project, with auto docs for the API and everything",
        version="0.0.1",
        openapi_url="/openapi.json"
    )

I'm also using custom exception handler in FastAPI:

from starlette_context import context

@app.exception_handler(AWSException)
async def unicorn_exception_handler(
    request: Request, exc: AWSException
) -> JSONResponse:

   user_id = context.get("user-id", default=None)
    
    print(user_id )

    return JSONResponse(
        status_code=400,
        content={"message": exc.message},
    )

But when i try to access context inside the custom exception handler i receive :

starlette_context.errors.ContextDoesNotExistError: You didn't use the required middleware or you're trying to access context object outside of the request-response cycle.

Any hints ?

Improving the __repr__ method of _Context

UserDict.__repr__ attempting to print the contents of _Context.data outside of a starlette app raises starlette_context.errors.ContextDoesNotExistError. Can we make the __repr__ return something that indicates that rather than raising exception?

Current problem:

# some route.py
"""
Module docstring
"""
from starlette_context import context
...

# in python console
import route
help(route)  # raises exception

Support for CorrelationIdPlugin and CorrelationIdPlugin still executing when application encounters an exception

Issue

When a route raises an exception and so returns a 500 Internal Server Error, x-request-id and x-correlation-id are not set.

The culprit seems to be

try:
response = await call_next(request)
for plugin in self.plugins:
await plugin.enrich_response(response)
finally:
_request_scope_context_storage.reset(_starlette_context_token)

This is similar to this comment - tiangolo/fastapi#397 (comment)

Expectation
I'd expect those headers to be set for all responses. One benefit of correlation ids is when there are errors, we can use the ids to track down the issue.

Steps to reproduce
Sample.
The "/" endpoint returns the x-request-id and x-correlation-id headers.
The "/error" does not.

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.requests import Request
from starlette.responses import JSONResponse

import uvicorn
from starlette_context import context, plugins
from starlette_context.middleware import ContextMiddleware

middleware = [
    Middleware(
        ContextMiddleware,
        plugins=(plugins.RequestIdPlugin(), plugins.CorrelationIdPlugin()),
    )
]

app = Starlette(debug=True, middleware=middleware)


@app.route("/")
async def index(request: Request):
    return JSONResponse(context.data)

@app.route("/error")
async def index(request: Request):
    raise RuntimeError()
    return JSONResponse(context.data)

uvicorn.run(app, host="0.0.0.0")

uninitialized context access raises LookupError

Here is a testcase:

>>> from starlette_context import context
>>> context
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/dmig/.pyenv/versions/3.8.2/lib/python3.8/collections/__init__.py", line 1014, in __repr__
    def __repr__(self): return repr(self.data)
  File "/home/dmig/.pyenv/versions/marty-services-3.8.2/lib/python3.8/site-packages/starlette_context/ctx.py", line 26, in data
    return _request_scope_context_storage.get()
LookupError: <ContextVar name='starlette_context' at 0x7f3b3be81c20>

Improved logging

Not sure if I was doing something wrong but it seems like the library could provide more useful logging. For instance I was passing non valid UUID's as the X-Request-ID and in this case all the logging I saw on my server was:

127.0.0.1:51990 - "POST /graphql/ HTTP/1.1" 400

Some descriptive log message would help.

PluginUUIDBase's force_new_uuid option seems broken

When using PluginUUIDBase's force_new_uuid option and setting it to True I'm always getting the same uuid. Which is the opposite of what I expect.

It looks like when self.value is None a new uuid is generated, and this works fine when for the first request. But subsequent request seem to be using the same id.

I suspect the code that needs to be fixed is missing a self.value = None before trying anything else here:

Are there plans to add type hints or library stubs for MyPy?

I'm using your library right now in a FastAPI app, and since everything's heavily typed in FastAPI (and Pydantic), I have MyPy enabled and it's using quite a strict configuration. Right now, it's complaining about the imports from starlette_context:

Code:

from starlette_context import context
from starlette_context.plugins import Plugin, RequestIdPlugin

Error:

Skipping analyzing 'starlette_context': found module but no type hints or library stubs  [import]
Skipping analyzing 'starlette_context.plugins': found module but no type hints or library stubs  [import]
See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports

I can always just suppress the error with # type: ignore but I was wondering if you were planning on adding type hints or stub packages/files, as recommended by MyPy here: https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-type-hints-for-third-party-library. I think that would be a better approach.

In case there's no plan for this yet, what would you say to a PR to add stub files? Basically, I think just adding the appropriate .pyi files next to each .py file would satisfy MyPy.

lower-case key name for plugin headers

Hello,
plugin system for headers extracting and placing it in context is fine, but I think i would be better if those headers in context could be accesible case insesitive or at least lower-case.
This would help in when I'm not sure what case this library used for keys.

For example x-request-id is here on key X-Request-ID but many users/devs would expect X-Request-Id or x-request-id and unless you dive in a code to explore what key your value resides on you just guessing (and in many cases incorrectly). This is counter intuitive.

Best case would be the same as for headers itself. Headers are stored as case insesitive multidict and it would be awesome to replicate this "case insesitiveness". Cheapest and more intuitive solution (than the current one) would be using lower keys for context keys based on headers. That will give me better chance to guess the key.

Thank you

Testing code that relies on context vars without a full test client / app

Sometimes it can be very useful to write tests for functions that rely on some data in the request context, but without using a full Starlette test client, as tests that are based on test clients are much less "lean" and do a lot of things beyond just running the code unit under test.

I think it would be very useful to document / add some helpers that enable a context manager that mocks a request / response cycle / middleware / etc. so it can be used in testing.

For now, I have done this in my tests (I'm using pytest fixtures, this is a very simplified example):

code under test

from starlette_context import context

def get_session_id():
    """Get the current session ID"""
    return context['session_id']

in conftest.py:

import pytest
from starlette_context import _request_scope_context_storage, context


@pytest.fixture()
def request_context():
    token = _request_scope_context_storage.set({})
    try:
        yield context
    finally:
        _request_scope_context_storage.reset(token)

in tests:

from myapp.session_utils import get_session_id 


def test_some_func_that_relies_on_context(request_context):
    request_context['session_id'] = 'foobar'
    assert get_session_id() == 'foobar'

Obviously, this is a bit hackish and it would be nice if there would be an official / documented way of doing this without accessing module internals.

Extract data from the request body

Are there future plans to support getting the context data from the request body?

Plugin.process_request isn't able to call async methods, so no way to use the request body in plugins now.

This feature would make benefits to log exceptions for instance.

Enforce that if request/correlation id provided, it must be a UUID otherwise generate new

Issue
Currently, any value is accepted.

Expectation
Only UUIDs should be accepted.

The could be implemented with a new kwarg on the Plugins so this isn't a breaking change for any users who currently accept request values that are not UUIDs.

Relevant source code

async def extract_value_from_header_by_key(
self, request: Request
) -> Optional[str]:
await super(
CorrelationIdPlugin, self
).extract_value_from_header_by_key(request)
if self.value is None:
self.value = uuid.uuid4().hex
return self.value

Happy to submit a PR if needed.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.