Fastapi: [BUG] RecursionError from response model in 0.47.1

Created on 20 Jan 2020  路  17Comments  路  Source: tiangolo/fastapi

Describe the bug

FastAPI 0.47.1 will not be able to start due to a RecursionError when there is a circular reference among models. The issue seems to originate from https://github.com/tiangolo/fastapi/pull/889. This works fine in 0.46.0.

Environment

  • OS: Windows
  • FastAPI Version: 0.47.1
  • Python version: 3.7.0

To Reproduce

from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel, Field


class Group(BaseModel):
    representative: Optional['Person'] = Field(None)


class Person(BaseModel):
    group: Optional[Group] = Field(None)


Group.update_forward_refs()


app = FastAPI()


@app.get('/group/{group_id}', response_model=Group)
def get_group(group_id):
    return []

Expected behavior

No exception

Actual output

Traceback (most recent call last):
  File "test.py", line 21, in <module>
    @app.get('/group/{group_id}', response_model=Group)
  File "D:\virtualenvs\test\lib\site-packages\fastapi\routing.py", line 494, in decorator
    callbacks=callbacks,
  File "D:\virtualenvs\test\lib\site-packages\fastapi\routing.py", line 438, in add_api_route
    callbacks=callbacks,
  File "D:\virtualenvs\test\lib\site-packages\fastapi\routing.py", line 275, in __init__
    ] = create_cloned_field(self.response_field)
  File "D:\virtualenvs\test\lib\site-packages\fastapi\utils.py", line 100, in create_cloned_field
    use_type.__fields__[f.name] = create_cloned_field(f)
  File "D:\virtualenvs\test\lib\site-packages\fastapi\utils.py", line 100, in create_cloned_field
    use_type.__fields__[f.name] = create_cloned_field(f)
  File "D:\virtualenvs\test\lib\site-packages\fastapi\utils.py", line 100, in create_cloned_field
    use_type.__fields__[f.name] = create_cloned_field(f)
  [Previous line repeated 981 more times]
  File "D:\virtualenvs\test\lib\site-packages\fastapi\utils.py", line 97, in create_cloned_field
    original_type.__name__, __config__=original_type.__config__
  File "D:\virtualenvs\test\lib\site-packages\pydantic\main.py", line 773, in create_model
    return type(model_name, (__base__,), namespace)
  File "D:\virtualenvs\test\lib\site-packages\pydantic\main.py", line 152, in __new__
    if issubclass(base, BaseModel) and base != BaseModel:
  File "D:\virtualenvs\test\lib\abc.py", line 143, in __subclasscheck__
    return _abc_subclasscheck(cls, subclass)
RecursionError: maximum recursion depth exceeded in comparison
bug

Most helpful comment

Thanks for the discussion here everyone!

This was fixed by @voegtlel in https://github.com/tiangolo/fastapi/pull/1164 :rocket: :tada:

It will be available in the next release (today in a couple of hours).

I'll re-open this issue to give @ysmu a chance to confirm it's fixed and close it.

All 17 comments

Okay, I think this could be safely fixed by using a cache to track models that have already been cloned, and then using that to short-circuit the recursion.

I think that should still be secure since only FastAPI would be able to generate instances of the cloned types, but I'm not 100% sure quite yet.


That said, if we could just implement dump_as_obj in pydantic and use that to eliminate the use of cloned fields, that would solve everything.

I'll take a look at it this week; if I can't quickly throw together dump_as_obj based on my previous work, or it looks like it may take a while for it to get merged into pydantic, I'll look into using the cache-based approach described above to patch this for the short term.


If anyone else wants to put together a PR to fix this I would be happy to review it and offer some guidance.

Hitting this as well. Can confirm that reverting to 0.46.0 fixes this.

New exception for above example when using fastapi 0.48.0:

Traceback (most recent call last):
  File "/home/user/Library/Python/3.7/bin/uvicorn", line 8, in <module>
    sys.exit(main())
  File "/home/user/Library/Python/3.7/lib/python/site-packages/click/core.py", line 764, in __call__
    return self.main(*args, **kwargs)
  File "/home/user/Library/Python/3.7/lib/python/site-packages/click/core.py", line 717, in main
    rv = self.invoke(ctx)
  File "/home/user/Library/Python/3.7/lib/python/site-packages/click/core.py", line 956, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/home/user/Library/Python/3.7/lib/python/site-packages/click/core.py", line 555, in invoke
    return callback(*args, **kwargs)
  File "/home/user/Library/Python/3.7/lib/python/site-packages/uvicorn/main.py", line 323, in main
    run(**kwargs)
  File "/home/user/Library/Python/3.7/lib/python/site-packages/uvicorn/main.py", line 346, in run
    server.run()
  File "/home/user/Library/Python/3.7/lib/python/site-packages/uvicorn/main.py", line 374, in run
    loop.run_until_complete(self.serve(sockets=sockets))
  File "uvloop/loop.pyx", line 1456, in uvloop.loop.Loop.run_until_complete
  File "/home/user/Library/Python/3.7/lib/python/site-packages/uvicorn/main.py", line 381, in serve
    config.load()
  File "/home/user/Library/Python/3.7/lib/python/site-packages/uvicorn/config.py", line 286, in load
    self.loaded_app = import_from_string(self.app)
  File "/home/user/Library/Python/3.7/lib/python/site-packages/uvicorn/importer.py", line 20, in import_from_string
    module = importlib.import_module(module_str)
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 728, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "./fastapi-bug-894.py", line 21, in <module>
    @app.get('/group/{group_id}', response_model=Group)
  File "/usr/local/lib/python3.7/site-packages/fastapi/routing.py", line 494, in decorator
    callbacks=callbacks,
  File "/usr/local/lib/python3.7/site-packages/fastapi/routing.py", line 438, in add_api_route
    callbacks=callbacks,
  File "/usr/local/lib/python3.7/site-packages/fastapi/routing.py", line 275, in __init__
    ] = create_cloned_field(self.response_field)
  File "/usr/local/lib/python3.7/site-packages/fastapi/utils.py", line 98, in create_cloned_field
    use_type.__fields__[f.name] = create_cloned_field(f)
  File "/usr/local/lib/python3.7/site-packages/fastapi/utils.py", line 98, in create_cloned_field
    use_type.__fields__[f.name] = create_cloned_field(f)
  File "/usr/local/lib/python3.7/site-packages/fastapi/utils.py", line 98, in create_cloned_field
    use_type.__fields__[f.name] = create_cloned_field(f)
  [Previous line repeated 945 more times]
  File "/usr/local/lib/python3.7/site-packages/fastapi/utils.py", line 96, in create_cloned_field
    use_type = create_model(original_type.__name__, __base__=original_type)
  File "/home/user/Library/Python/3.7/lib/python/site-packages/pydantic/main.py", line 780, in create_model
    return type(model_name, (__base__,), namespace)
  File "/home/user/Library/Python/3.7/lib/python/site-packages/pydantic/main.py", line 153, in __new__
    fields.update(deepcopy(base.__fields__))
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/copy.py", line 150, in deepcopy
    y = copier(x, memo)
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/copy.py", line 240, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/copy.py", line 180, in deepcopy
    y = _reconstruct(x, memo, *rv)
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/copy.py", line 280, in _reconstruct
    state = deepcopy(state, memo)
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/copy.py", line 150, in deepcopy
    y = copier(x, memo)
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/copy.py", line 220, in _deepcopy_tuple
    y = [deepcopy(a, memo) for a in x]
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/copy.py", line 220, in <listcomp>
    y = [deepcopy(a, memo) for a in x]
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/copy.py", line 150, in deepcopy
    y = copier(x, memo)
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/copy.py", line 240, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/copy.py", line 180, in deepcopy
    y = _reconstruct(x, memo, *rv)
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/copy.py", line 280, in _reconstruct
    state = deepcopy(state, memo)
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/copy.py", line 150, in deepcopy
    y = copier(x, memo)
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/copy.py", line 220, in _deepcopy_tuple
    y = [deepcopy(a, memo) for a in x]
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/copy.py", line 220, in <listcomp>
    y = [deepcopy(a, memo) for a in x]
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/copy.py", line 150, in deepcopy
    y = copier(x, memo)
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/copy.py", line 240, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/copy.py", line 150, in deepcopy
    y = copier(x, memo)
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/copy.py", line 238, in _deepcopy_dict
    memo[id(x)] = y
RecursionError: maximum recursion depth exceeded while calling a Python object

Hitting this as well. Can confirm that reverting to 0.46.0 fixes this.

Same issue/workarround.
OS: Windows and MacOS
FastAPI Version: 0.48.0
Python version: 3.7.3

~However we don't have the issue on MacOS.~

I've been busy so haven't had a lot of time to look into this just yet.

@zamiramir that's actually very useful information -- I suspect it is related to the cythonization of pydantic.

Any chance you could run from pydantic.version import version_info; print(version_info()) in both environments (MacOS and non-MacOS), and share the output?

I originally reproduced the issue on MacOS (both 0.47 and 0.48). Latest version of Catalina.

@jmagnusson it would be most helpful if you could share the output of the version info function as described above, which includes information about whether pydantic was compiled, etc.

I had pydantic 1.3 installed where the command you specified did not work. However, with 1.4 I get:

python3 -c 'from pydantic.version import version_info; print(version_info())'
             pydantic version: 1.4
            pydantic compiled: False
                 install path: /usr/local/lib/python3.7/site-packages/pydantic
               python version: 3.7.6 (default, Dec 30 2019, 19:38:26)  [Clang 11.0.0 (clang-1100.0.33.16)]
                     platform: Darwin-19.3.0-x86_64-i386-64bit
     optional deps. installed: ['typing-extensions', 'email-validator']

Same issue with RecursionError: maximum recursion depth exceeded while calling a Python object after Pydantic 1.4 upgrade.

Uninstall pydantic and installed cython first, then pydantic. Still shows pydantic compiled: False.

I've been busy so haven't had a lot of time to look into this just yet.

@zamiramir that's actually very useful information -- I suspect it is related to the cythonization of pydantic.

Any chance you could run from pydantic.version import version_info; print(version_info()) in both environments (MacOS and non-MacOS), and share the output?

Sorry just tested it again, and got the same issue on MacOS. Might have been an older version of FastAPI.

I can also confirm this, reverting to 0.46.0 works.

I've also hit this issue, confirming that downgrading to 0.46.0 works.

Still occurs in fastapi==0.52.0.

Hi there, I've suggested another MR to fix this issue. The one of @mateuszz0000 was missing the recursive field resolution. EDIT: Coverage showed me I was wrong here and used an older version of fastapi for my initial test of the fix 馃檲 Works without that part (so removed it from the commit).

Still, I think local cloning (i.e. keeping dicts of what was cloned) should be preferred before setting another hidden attribute?

Thanks for the discussion here everyone!

This was fixed by @voegtlel in https://github.com/tiangolo/fastapi/pull/1164 :rocket: :tada:

It will be available in the next release (today in a couple of hours).

I'll re-open this issue to give @ysmu a chance to confirm it's fixed and close it.

Fix works for me. Thank you!

Ah thanks for fixing this, our front-end team really appreciates it ;)

Was this page helpful?
0 / 5 - 0 ratings