Skip to content

Sentry FastAPI middleware hangs if user middleware raises an exception #4827

@hakanutku

Description

@hakanutku

How do you use Sentry?

Sentry Saas (sentry.io)

Version

2.38.0

Steps to Reproduce

Sentry FastAPI middleware hangs if user middleware raises an exception. If I comment out sentry_sdk.init() call, I get the correct 422 response.

fastapi==0.115.12
starlette==0.46.2

import gzip
import json
import typing
import zlib

import sentry_sdk
from fastapi import FastAPI, Request
from fastapi.testclient import TestClient
from sentry_sdk.integrations.fastapi import FastApiIntegration
from sentry_sdk.integrations.starlette import StarletteIntegration
from starlette import status
from starlette.datastructures import Headers
from starlette.exceptions import HTTPException
from starlette.types import ASGIApp, Message, Receive, Scope, Send


class GZipRequestMiddleware:
    def __init__(self, app: ASGIApp):
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope["type"] == "http":
            headers = Headers(scope=scope)

            if headers.get("Content-Encoding") == "gzip":
                reader = GZipRequestReader(self.app)
                await reader(scope, receive, send)
                return

        await self.app(scope, receive, send)


class GZipRequestReader:
    def __init__(self, app: ASGIApp) -> None:
        self.app = app
        self.receive: Receive = unattached_receive
        self.scope: Scope = {}
        self.actual_body_size = 0
        self.decompressor = zlib.decompressobj(16 + zlib.MAX_WBITS)

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        self.scope = scope
        self.receive = receive
        await self.app(scope, self._receive_gzipped_request, send)

    async def _receive_gzipped_request(self) -> Message:
        message = await self.receive()
        body = message.get("body", b"")
        if body:
            try:
                message["body"] = self.decompressor.decompress(body)
            except zlib.error as e:
                raise HTTPException(
                    status.HTTP_422_UNPROCESSABLE_ENTITY, "Compressed request body is malformed!"
                ) from e
            else:
                self.actual_body_size += len(message["body"])

        if not message.get("more_body", False):
            if not self.decompressor.eof:
                raise HTTPException(
                    status.HTTP_422_UNPROCESSABLE_ENTITY, "Compressed request body is truncated or incomplete!"
                )

            headers_copy = Headers(scope=self.scope).mutablecopy()
            del headers_copy["Content-Encoding"]
            headers_copy["Content-Length"] = str(self.actual_body_size)
            self.scope["headers"] = headers_copy.raw

        return message


async def unattached_receive() -> typing.NoReturn:
    raise RuntimeError("awaitable not set")  # noqa: EM101, TRY003 # pragma: no cover


if __name__ == "__main__":
    sentry_sdk.init(
        dsn="",
        send_default_pii=True,
        traces_sample_rate=1.0,
        environment="production",
        integrations=[
            FastApiIntegration(failed_request_status_codes={*range(500, 600)}),
            StarletteIntegration(failed_request_status_codes={*range(500, 600)}),
        ],
    )
    app = FastAPI()
    app.add_middleware(GZipRequestMiddleware)

    @app.post("/test")
    async def test(request: Request):
        print(await request.body())
        return await request.body()

    test_client = TestClient(app)

    uncompressed_body = '{"test": "test"}'  # Invalid uncompressed body. Should return 422
    response = test_client.post(
        "/test", content=uncompressed_body, headers={"Content-Encoding": "gzip", "Content-Type": "application/json"}
    )

    print(response.status_code)
    print(response.text)

Expected Result

Request should not hang and return 422 with "Compressed request body is malformed!" error message.

Actual Result

Cannot get response or timeout exception. It just hangs indefinitely.

Metadata

Metadata

Labels

Projects

Status

No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions