#Strange behavior

1 messages Β· Page 1 of 1 (latest)

mild pawn
#

In FastApi test fails as expected but in litestar test success
FastApi example:

from fastapi import FastAPI
from fastapi.testclient import TestClient

app = FastAPI()

@app.get("/always-error")
def always_error() -> str:
    raise RuntimeError("error")


def test_always_error():
    with TestClient(app=app) as client:
        client.get("/always-error")

Litestar example:

from litestar import Litestar, get
from litestar.testing import TestClient


@get("/always-error")
def always_error() -> str:
    raise RuntimeError("error")


app = Litestar(route_handlers=[always_error])


def test_always_error():
    with TestClient(app=app) as client:
        client.get("/always-error")

How to achieve the same behavior from litestar as in fastapi?

limpid pawnBOT
#
Notes for Strange behavior
At your assistance

@mild pawn

No Response?

If no response in a reasonable time, ping @Member.

Closing

To close, type !solve or byte solve.

MCVE

Please include an MCVE so that we can reproduce your issue locally.

pine oak
mild pawn
urban vineBOT
#

starlette/middleware/errors.py lines 184 to 187

# We always continue to raise the exception.
# This allows servers to log the error, or allows test clients
# to optionally raise the error within the test case.
raise exc
obsidian pewter
#
from litestar import Litestar, get
from litestar.testing import TestClient


@get("/always-error")
def always_error() -> str:
    raise RuntimeError("error")


app = Litestar(route_handlers=[always_error])


def test_always_error():
    with TestClient(app=app) as client:
        response = client.get("/always-error")
        assert response.status_code == 500
        assert response.text == '{"status_code":500,"detail":"Internal Server Error"}'
#

is this not the behavior you'd expect?

mild pawn
#

No

#

This fails on assert stmt without correct stacktrace like in example with fastapi

gentle kelp
#

Pass raise_server_exceptions=True to the test client

#

Litestar defaults to behaving the same way with the test client as it does when running with a proper server, while the Starlette behaviour differs

mild pawn
#

raise_server_exceptions set to true by default. If I add them manual nothing changes. Lifestart simply does not re-raise the exception, unlike fastapi. Look at the source code of both frameworks.

urban vineBOT
#

litestar/testing/transport.py lines 171 to 173

except BaseException as exc:
    if self.raise_server_exceptions:
        raise exc
gentle kelp
#

Ah, I think there's a misunderstanding

#

Litestar's test client will never raise exceptions that, when not running the app with a test client, would result in an exception response

#

It's simply a different design approach than what starlette/fastapi are doing

mild pawn
#

The problem is not with the test client. The problem is with the ExceptionHandlerMiddleware. If I add raise at the end of the __call__ method, the problem will be solved.

gentle kelp
#

There is no problem. The middleware is working as intended

#

You just want it to work differently πŸ™‚

#

If the middleware were to raise exceptions, your application would crash every time there's an exception. Which I'm assuming isn't what you want, and instead, you'd prefer to be able to a) handle that exception or b) return a 500 - Internal Server Error response

#

The discrepancy here lies in the fact that the test client behaves exactly like running the app on a regular server would

#

With raise_server_exceptions=True, exceptions raised before the app starts will cause it to raise an exception and crash, and exceptions raised during request handling result in a 500 response

#

Other than "FastAPI behaves this way", can you maybe explain what your use case for the behaviour you're looking for is? Perhaps we can figure out another way to handle that πŸ™‚

obsidian pewter
#

Thanks, @gentle kelp. I was going to ask why this behavior was strange, but you've covered the bases.

mild pawn
pine oak
mild pawn
mild pawn
#

I have now added raise and launched the application via unicorn and got stacktrace. The server responded as expected. If remove raise then server just logs INFO: 127.0.0.1:52696 - "GET /api/v1/hello?name= HTTP/1.1" 500 Internal Server Error

obsidian pewter
#

Again, it's not strange behavior, but expected behavior

gentle kelp
#

Yes, FastAPI handles exceptions when running with a ASGI server

#

But it does not when running with the test client

#

There is a discrepancy in behaviour between the production and test environment

#

Litestar, intentionally, does not have this discrepancy

mild pawn
gentle kelp
mild pawn
gentle kelp
obsidian pewter
gentle kelp
gentle kelp
#

My point here was, that this differs from running it with the test client

#

Let me try to illustrate why that is, in our opinion, not the best design choice and why Litestar differs

#
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

app = FastAPI()


@app.get("/always-error")
def always_error() -> str:
    raise RuntimeError("error")


def test_always_error():
    with pytest.raises(RuntimeError):
        with TestClient(app=app) as client:
            client.get("/always-error")
#

This test will pass

#

If you deploy your application, that endpoint will behave differently than your tests asserts it to

#

This means that your test isn't testing the actual behaviour of your application. It's closer to testing a mock

#

In Litestar, this test will not pass. Instead, you'll get a 500 response. Just like you would if you were talking to the server with a real HTTP client

#

Now the logging part is an entirely different debate. You can get the stack trace

#

If you do what @obsidian pewter suggested earlier and build a test case that actually fails, you'll get the traceback you expect


from litestar import get, Litestar
from litestar.testing import TestClient


@get("/")
async def handler() -> None:
    raise RuntimeError()


app = Litestar([handler], debug=True)


def test() -> None:
    with TestClient(app) as client:
        response = client.get("/")
        assert response.status_code == 204
mild pawn
# gentle kelp But indulging your question: That is just the behaviour you've been describing e...

No, please try it by yourself on your computer
Source code of the app

import uvicorn
from fastapi import FastAPI

app = FastAPI()


@app.get("/always-error")
def always_error() -> str:
    raise RuntimeError("error")

uvicorn.run(app)

If you run it and call method you got stacktrece contains

ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "D:\projects\,,,\.venv\Lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 403, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        self.scope, self.receive, self.send

That handler in uvicorn not in fastapi

gentle kelp
#

Okay, so I'm misremembering Starlette's exception handler stack. My point still stands. The behaviour is different in a test environment and a deployment

mild pawn
gentle kelp
#

Since that's all you have to do to get the behaviour you're looking for as far as I can tell

mild pawn
gentle kelp
#
def test() -> None:
    with TestClient(app) as client:
        response = client.get("/")
        assert response.status_code == 204

This is the test you'd have to write

#

You'll get a traceback from this

mild pawn
#

No

#

I got incorrect stack

gentle kelp
#

Since your app will never return a 500, and instead raise

#

This behaviour differs from the deployment, where any HTTP client will receive a 500 response

gentle kelp
# mild pawn I got incorrect stack
Traceback (most recent call last):
  File "/home/.../litestar/litestar/middleware/_internal/exceptions/middleware.py", line 159, in __call__
    await self.app(scope, receive, capture_response_started)
  File "/home/.../litestar/litestar/_asgi/asgi_router.py", line 100, in __call__
    await asgi_app(scope, receive, send)
  File "/home/.../litestar/litestar/routes/http.py", line 81, in handle
    response = await self._get_response_for_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/..../litestar/litestar/routes/http.py", line 133, in _get_response_for_request
    return await self._call_handler_function(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/.../litestar/litestar/routes/http.py", line 153, in _call_handler_function
    response_data, cleanup_group = await self._get_response_data(
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/.../litestar/litestar/routes/http.py", line 200, in _get_response_data
    data = await route_handler.fn(**parsed_kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/.../litestar/t.py", line 25, in handler
    raise RuntimeError()
RuntimeError
``` that's the traceback
#

What exactly is incorrect about it?

mild pawn
gentle kelp
#

Are you running the exact code I shared? πŸ™‚

#

More importantly, are you setting the debug=True flag?

#

by default, tracebacks are only logged when in debug mode. If you always want to log all tracebacks, you'll have to pass logging_config=LoggingConfig(log_exceptions="always"))

mild pawn
gentle kelp
#

Okay

#

Not sure what to tell you?

#

That it's bad is clearly a personal opinion, and not a bug or "strange behaviour" (as it's the intended behaviour πŸ™‚)

#

So there's not really anything that can be done here?

mild pawn
#

Why not just copy raise?

obsidian pewter
#

Because we aren’t fastapi?

gentle kelp
# mild pawn Why not just copy `raise`?
  1. Changing this behaviour now would be a major breaking change
  2. I've explained the reasoning above. You can disagree with this, but if it's that much of an inconvenience for you, I'd suggest you use FastAPI. We are not trying to be compatible with FastAPI / Starlette, and this specifically is a deliberate divergence from the way they handle things
mild pawn