Skip to content

Conversation

@vprivat-ads
Copy link

@vprivat-ads vprivat-ads commented Dec 2, 2025

Description:

When we launch stac-fastapi-pgstac with invalid postgresql credentials, the readpool will not be created, but our application will try to close it and raise this exception:

asyncpg.exceptions.InvalidPasswordError: password authentication failed for user "catalog"
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
  File "/usr/local/lib/python3.11/site-packages/starlette/datastructures.py", line 685, in __getattr__
    return self._state[key]
           ~~~~~~~~~~~^^^^^
KeyError: 'readpool'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
  File "/usr/local/lib/python3.11/site-packages/starlette/routing.py", line 694, in lifespan
    async with self.lifespan_context(app) as maybe_state:
  File "/usr/local/lib/python3.11/contextlib.py", line 210, in __aenter__
    return await anext(self.gen)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/rs_server_catalog/app.py", line 168, in lifespan
    await close_db_connection(my_app)
  File "/usr/local/lib/python3.11/site-packages/stac_fastapi/pgstac/db.py", line 86, in close_db_connection
    await app.state.readpool.close()
          ^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/starlette/datastructures.py", line 688, in __getattr__
    raise AttributeError(message.format(self.__class__.__name__, key))
AttributeError: 'State' object has no attribute 'readpool'
ERROR:    Application startup failed. Exiting.

PR Checklist:

  • pre-commit hooks pass locally
  • Tests pass (run make test)
  • Documentation has been updated to reflect changes, if applicable, and docs build successfully (run make docs)
  • Changes are added to the CHANGELOG.

@vprivat-ads vprivat-ads force-pushed the fix/app-state-no-readpool branch from ac82a33 to b0b5c61 Compare December 2, 2025 10:25
@vincentsarago
Copy link
Member

@vprivat-ads I can't replicate locally, my application exit silently.

On my local env, the app will tell Application startup failed. Exiting and exit directly.

asyncpg.exceptions.InvalidCatalogNameError: database "postgisyo" does not exist

ERROR:    Application startup failed. Exiting.

and thus won't perform the close_db_connection step

@vprivat-ads vprivat-ads force-pushed the fix/app-state-no-readpool branch from b0b5c61 to 41dc43f Compare December 3, 2025 10:46
@vprivat-ads
Copy link
Author

Maybe it happens only with custom lifespans, we currently have this in our application:

from stac_fastapi.pgstac.app import with_transactions
from stac_fastapi.pgstac.db import close_db_connection, connect_to_db

@asynccontextmanager
async def lifespan(my_app: FastAPI):
    """The lifespan function."""
    try:
        # Connect to the databse
        db_info = f"'{env['POSTGRES_USER']}@{env['POSTGRES_HOST']}:{env['POSTGRES_PORT']}'"
        while True:
            try:
                await connect_to_db(my_app, add_write_connection_pool=with_transactions)
                logger.info("Reached %r database on %s", env["POSTGRES_DB"], db_info)
                break
            except ConnectionRefusedError:
                logger.warning("Trying to reach %r database on %s", env["POSTGRES_DB"], db_info)

                # timeout gestion if specified
                if my_app.state.pg_timeout is not None:
                    my_app.state.pg_timeout -= my_app.state.pg_pause
                    if my_app.state.pg_timeout < 0:
                        sys.exit("Unable to start up catalog service")
                await asyncio.sleep(my_app.state.pg_pause)

        common_settings.set_http_client(httpx.AsyncClient(timeout=DEFAULT_TIMEOUT_CONFIG))

        # Run the data lifecycle management as an automatic periodic task
        lifecycle.run()

        yield

    finally:
        await lifecycle.cancel()
        await close_db_connection(my_app)
        await common_settings.del_http_client()

@vincentsarago
Copy link
Member

Yeah, I think it's because you have a Try/Finally block. if an exception is raised before the yield, you'll still go to the close_db_connection while with the normal lifespan function we won't call it.

I'll approve the PR because it's technically correct 😅

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