question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

FastAPI 0.69 app with SQLAlchemy hangs in pytest when HTTPError is raised

See original GitHub issue

First Check

  • I added a very descriptive title to this issue.
  • I used the GitHub search to find a similar issue and didn’t find it.
  • I searched the FastAPI documentation, with the integrated search.
  • I already searched in Google “How to X in FastAPI” and didn’t find any information.
  • I already read and followed all the tutorial in the docs and didn’t find an answer.
  • I already checked if it is not related to FastAPI but to Pydantic.
  • I already checked if it is not related to FastAPI but to Swagger UI.
  • I already checked if it is not related to FastAPI but to ReDoc.

Commit to Help

  • I commit to help with one of those options 👆

Example Code

"""
test_fastapi.py

Requires:

- python3
- docker

Setup:

  virtualenv -p /usr/bin/python3 .venv
  source .venv/bin/activate
  pip install fastapi==0.69 sqlalchemy mysqlclient pytest requests

Usage:

  pytest test_fastapi.py -vvs
"""
import subprocess
import time

import pytest
from fastapi import Body, FastAPI, HTTPException, Path
from fastapi.testclient import TestClient
from pydantic import BaseModel
from sqlalchemy import (
    Column,
    Integer,
    MetaData,
    String,
    Table,
    create_engine,
    insert,
    select,
)


# Config for the docker container
CONTAINER = "mariadb-fastapi"
MYSQL_PORT = "33072"
MYSQL_DATABASE = "test"
MYSQL_ROOT_PASSWORD = "rootmysql"
DB_URL = f"mysql://root:{MYSQL_ROOT_PASSWORD}@127.0.0.1:{MYSQL_PORT}/{MYSQL_DATABASE}"

# SQLAlchemy
ENGINE = create_engine(DB_URL)
METADATA = MetaData()
T = Table(
    "job",
    METADATA,
    Column("id", Integer(), primary_key=True),
    Column("name", String(length=50)),
)


# FastAPI model and app
class Job(BaseModel):
    name: str


app = FastAPI()


@app.get("/jobs/{job_id}")
def get_job(job_id: int = Path(...)):
    conn = ENGINE.connect()
    stmt = select([T]).with_for_update().where(T.c.id == job_id)
    conn.execute(stmt).fetchone()  # The result does not matter
    # This exception that is raised in the first test causes the second test to
    # hang during its setup.
    # If we do "return None" instead, the second test will be run.
    # return None
    raise HTTPException(404, f'Job with ID "{job_id}" not found')


@app.post("/jobs")
def create_job(job: Job = Body(...)):
    with ENGINE.begin() as conn:
        ins = insert(T).values(**job.dict())
        conn.execute(ins)


#
# Tests
#


@pytest.fixture(autouse=True, scope="module")
def mariadb_docker():
    with subprocess.Popen(
        [
            "docker",
            "run",
            "--rm",
            "--env",
            f"MYSQL_DATABASE={MYSQL_DATABASE}",
            "--env",
            f"MYSQL_ROOT_PASSWORD={MYSQL_ROOT_PASSWORD}",
            f"--name={CONTAINER}",
            f"--publish={MYSQL_PORT}:3306",
            "mariadb",
        ]
    ):
        print("connecting ", end="")
        while True:
            try:
                ENGINE.connect()
                print("done")
                break
            except:
                print(".", end="")
                time.sleep(1)
        yield
        subprocess.run(["docker", "stop", CONTAINER], check=False)


@pytest.fixture
def session():
    print("SESSION drop all")
    METADATA.drop_all(ENGINE)
    print("SESSION create all")
    METADATA.create_all(ENGINE)
    print("SESSION make client")
    client = TestClient(app)
    print("SESSION setup done")
    return client


def test_function_raising_error(session):
    print("TEST starting")
    response = session.get(f"/jobs/2")
    assert response.status_code == 404, response.text
    print("TEST end")


def test_hangs_in_setup(session):
    print("TEST starting")
    response = session.post(f"/jobs", data={"name": "test"})
    assert response.status_code == 422
    print("TEST end")
import subprocess
import time

import pytest
from fastapi import Body, FastAPI, HTTPException, Path
from fastapi.testclient import TestClient
from pydantic import BaseModel
from sqlalchemy import (
    Column,
    Integer,
    MetaData,
    String,
    Table,
    create_engine,
    insert,
    select,
)


# Config for the docker container
CONTAINER = "mariadb-fastapi"
MYSQL_PORT = "33072"
MYSQL_DATABASE = "test"
MYSQL_ROOT_PASSWORD = "rootmysql"
DB_URL = f"mysql://root:{MYSQL_ROOT_PASSWORD}@127.0.0.1:{MYSQL_PORT}/{MYSQL_DATABASE}"

# SQLAlchemy
ENGINE = create_engine(DB_URL)
METADATA = MetaData()
T = Table(
    "job",
    METADATA,
    Column("id", Integer(), primary_key=True),
    Column("name", String(length=50)),
)


# FastAPI model and app
class Job(BaseModel):
    name: str


app = FastAPI()


@app.get("/jobs/{job_id}")
def get_job(job_id: int = Path(...)):
    conn = ENGINE.connect()
    stmt = select([T]).with_for_update().where(T.c.id == job_id)
    row = conn.execute(stmt).fetchone()
    if row is None:
        # This exception that is raised in the first test causes the second test to
        # hang during its setup.
        # If we do "return None" instead, the second test will be run.
        # return None
        raise HTTPException(404, f'Job with ID "{job_id}" not found')
    return row


@app.post("/jobs")
def create_job(job: Job = Body(...)):
    with ENGINE.begin() as conn:
        ins = insert(T).values(**job.dict())
        conn.execute(ins)


#
# Tests
#


@pytest.fixture(autouse=True, scope="module")
def mariadb_docker():
    with subprocess.Popen(
        [
            "docker",
            "run",
            "--rm",
            "--env",
            f"MYSQL_DATABASE={MYSQL_DATABASE}",
            "--env",
            f"MYSQL_ROOT_PASSWORD={MYSQL_ROOT_PASSWORD}",
            f"--name={CONTAINER}",
            f"--publish={MYSQL_PORT}:3306",
            "mariadb",
        ]
    ):
        print("connecting ", end="")
        while True:
            try:
                ENGINE.connect()
                print("done")
                break
            except:
                print(".", end="")
                time.sleep(1)
        yield
        subprocess.run(["docker", "stop", CONTAINER], check=False)


@pytest.fixture
def session():
    print("SESSION drop all")
    METADATA.drop_all(ENGINE)
    print("SESSION create all")
    METADATA.create_all(ENGINE)
    print("SESSION make client")
    client = TestClient(app)
    print("SESSION setup done")
    return client


def test_function_raising_error(session):
    print("TEST starting")
    response = session.get(f"/jobs/2")
    assert response.status_code == 404, response.text
    print("TEST end")


def test_hangs_in_setup(session):
    print("TEST starting")
    response = session.post(f"/jobs", data={"name": "test"})
    assert response.status_code == 422
    print("TEST end")

Description

Starting with FastAPI 0.69, pytest tests started to hang under obscure conditions. The same code worked as expected with FastAPI 0.68.2.

I had issues with hanging tests in multiple projects. One was related to working with temp file handlers and the other one is shown in the example and related to working with SQLAlchemy/MariaDB.

In the example, the first test calls an endpoint that does an SQLAlchemy query and raises an HTTPException.

Any test after that will hang during setup (when METADATA.drop_all() is called).

This is what I get when I run the example:

$ pytest test_fastapi.py -vvs
=========================================================================================== test session starts ============================================================================================
platform linux -- Python 3.9.7, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 -- /tmp/.venv/bin/python
cachedir: .pytest_cache
rootdir: /tmp
plugins: anyio-3.4.0
collected 2 items

test_fastapi.py::test_function_raising_error connecting done
SESSION drop all
SESSION create all
docker: Error response from daemon: Conflict. The container name "/mariadb-fastapi" is already in use by container "70717ec0e7ee7f8b721c2866a79eca73cfcd7bb3758978a9b7fe8e9b8f008786". You have to remove (or rename) that container to be able to reuse that name.
See 'docker run --help'.
SESSION make client
SESSION setup done
TEST starting
TEST end
PASSED
test_fastapi.py::test_hangs_in_setup SESSION drop all

Operating System

Linux

Operating System Details

Fedora 34

FastAPI Version

0.69.0

Python Version

3.9.7

Additional Context

Docker 20.10.11

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:5

github_iconTop GitHub Comments

2reactions
Feijocommented, Feb 11, 2022

I’m having a similar issue in version 0.73.0

0reactions
sscherfkecommented, May 19, 2022

Yup, that solves it for me. Thx. 😃

Read more comments on GitHub >

github_iconTop Results From Across the Web

fastapi (module) sqlalchemy import sqlalchemy could not be ...
py from fastapi import FastAPI from lock import FileLock app = FastAPI() lock ... 0.69 app with SQLAlchemy hangs in pytest when HTTPError...
Read more >
How to test httpError in fastAPI with pytest? - Stack Overflow
Now I want to know: how can I raise HTTPException and test it on purpose? #main.py (FAST API) @app.get('/hello') def ...
Read more >
Release Notes - FastAPI
If you are using response_model with some type that doesn't include None but the function is returning None , it will now raise...
Read more >
[FastAPI/SQLAlchemy/Pytest] Database row not updating ...
[FastAPI/SQLAlchemy/Pytest] Database row not updating during unit testing ... lambda: db with TestClient(app) as c: yield c.
Read more >
Fastapi - ormar
from typing import List, Optional import databases import sqlalchemy from fastapi import FastAPI import ormar app = FastAPI() metadata = sqlalchemy.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found