Skip to content

GZipResponder has been rewritten so that it no longer throws not raised exception warning#2869

Closed
nesb1 wants to merge 1 commit intoKludex:masterfrom
nesb1:master
Closed

GZipResponder has been rewritten so that it no longer throws not raised exception warning#2869
nesb1 wants to merge 1 commit intoKludex:masterfrom
nesb1:master

Conversation

@nesb1
Copy link

@nesb1 nesb1 commented Feb 14, 2025

Summary

Hello, thank you to everyone involved for this wonderful project. I'm glad to have the opportunity to contribute in some way.

I'm using FastAPI in my web service, and after upgrading to Python 3.13, I've noticed a large number of warnings like this:

Exception ignored in: <gzip on 0x7efc933315d0>
Traceback (most recent call last):
  File "/usr/local/lib/python3.13/gzip.py", line 359, in close
    fileobj.write(self.compress.flush())
ValueError: I/O operation on closed file.

I spent a considerable amount of time trying to understand where this error actually comes from and realized that it's related to GzipMiddleware. I saw the issue #2615, but honestly, I didn't understand why it was marked as completed.

This error occurs because the BytesIO object is closed before the GzipFile. I suggest using these two objects through a context manager, as mentioned in the CPython issue python/cpython#122727.

I also spent a lot of time writing a test for this, so let me explain it. I couldn't write the test similarly to other tests in gzip_middleware, where requests are made through the test client; for some reason, the issue would not reproduce this way, possibly due to garbage collection peculiarities. However, in the current test, it's evident that the problem reproduces quite easily—one only needs to create a GzipResponder object and delete it. The test was written before I fixed the implementation, and I confirmed that it indeed did not work.

Checklist

  • I understand that this PR may be closed in case there was no previous discussion. (This doesn't apply to typos!)
  • I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
  • I've updated the documentation accordingly.

…gs like `Exception ignored in: <gzip on 0x7f0fd0276380>`
@Kludex
Copy link
Owner

Kludex commented Feb 14, 2025

I spent a considerable amount of time trying to understand where this error actually comes from and realized that it's related to GzipMiddleware. I saw the issue #2615, but honestly, I didn't understand why it was marked as completed.

If you follow the linked issues/PRs, you'll see on the CPython issue, Thomas said that we solved in Starlette.

It was solved on #2662.

@Kludex
Copy link
Owner

Kludex commented Feb 14, 2025

I guess you are using an old version of Starlette? Please share the whole traceback instead of only the gzip module part...

@Kludex
Copy link
Owner

Kludex commented Feb 14, 2025

Closing this PR since the user didn't share a way to reproduce, nor the package versions, nor marked the PR checklist.

Please create a discussion or reply here with the missing information, and we can reopen.

@Kludex Kludex closed this Feb 14, 2025
@nesb1
Copy link
Author

nesb1 commented Feb 14, 2025

@Kludex you can copy my test to reproduce this issue on master and python 3.13.1.

@nesb1
Copy link
Author

nesb1 commented Feb 14, 2025

@Kludex The difficulty lies in the fact that I cannot provide a detailed traceback; all I have is a trace from the gzip module. Unraised Exceptions do not have a detailed description of the call stack, as the problem occurs at the moment the garbage collector begins its work, and it can start at any time. If you try to print the current stack before the garbage collector starts, there will be calls unrelated to the problem.

@Kludex
Copy link
Owner

Kludex commented Feb 14, 2025

Please provided an MRE.

@nesb1
Copy link
Author

nesb1 commented Feb 14, 2025

@Kludex you can copy my test to reproduce this issue on master and python 3.13.1.

MRE is an unit test in this mr, this test fails on master, but works with my changes

@Kludex
Copy link
Owner

Kludex commented Feb 14, 2025

That's not an MRE.

@nesb1
Copy link
Author

nesb1 commented Feb 14, 2025

@Kludex

Environment

Python 3.13.1
Ubuntu 24

Code

from starlette.applications import Starlette
from starlette.middleware.gzip import GZipResponder

app = Starlette()

responder = GZipResponder(app=app, minimum_size=500)
del responder

Actual output:

Exception ignored in: <gzip on 0x710205b79e40>
Traceback (most recent call last):
  File "/home/evgeny/.local/share/uv/python/cpython-3.13.1-linux-x86_64-gnu/lib/python3.13/gzip.py", line 359, in close
    fileobj.write(self.compress.flush())
ValueError: I/O operation on closed file.

Expected output:

no warnings are expected in stdout

@Kludex
Copy link
Owner

Kludex commented Feb 14, 2025

Why would you use GZipResponder standalone?

This doesn't reproduce:

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.gzip import GZipMiddleware
from starlette.requests import Request
from starlette.responses import PlainTextResponse
from starlette.routing import Route


async def homepage(request: Request) -> PlainTextResponse:
    return PlainTextResponse("Hello, world" * 100)


app = Starlette(routes=[Route("/", homepage)], middleware=[Middleware(GZipMiddleware)])

@nesb1
Copy link
Author

nesb1 commented Feb 15, 2025

@Kludex
Similarly to your example, in production code, I only specify that GzipMiddleware should be used and do not use GZipResponder, as it is an internal class. However, as I mentioned, the problem is difficult to reproduce, but over time I realized that the issue is indeed with GZipResponder. The code of this class shows that if the __call__ method is not invoked, it will encounter a termination problem since the context managers for BytesIO and GzipFile will not be called. My example above localizes this problem.

I have also made progress in studying this issue, and I am ready to provide a higher-level example:

from asyncio import CancelledError

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.gzip import GZipMiddleware, GZipResponder
from starlette.requests import Request
from starlette.responses import PlainTextResponse
from starlette.routing import Route
from starlette.testclient import TestClient


async def homepage(request: Request) -> PlainTextResponse:
    return PlainTextResponse("Hello, world" * 100)


def gzip_responder_call_with_cancelled_error(*args, **kwargs):
    raise CancelledError()


GZipResponder.__call__ = gzip_responder_call_with_cancelled_error

app = Starlette(routes=[Route("/", homepage)], middleware=[Middleware(GZipMiddleware)])

test_client: TestClient = TestClient(
    app, raise_server_exceptions=False
)

test_client.get("/", headers={"accept-encoding": "gzip"})

As I mentioned earlier, the problem is reproducible if an instance of GzipResponder is created but its __call__ method is not invoked. Given that in an asynchronous application, a CancelledError can occur with any await call, this issue can be reproduced in this way: running this code will generate a warning in the console.

@nesb1
Copy link
Author

nesb1 commented Feb 19, 2025

@Kludex I think it should be reopened, as it is relevant.

@Kludex
Copy link
Owner

Kludex commented Feb 19, 2025

I don't seem to be able to reopen this PR.

Can you create an issue, please? I'll investigate when I have time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants