diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index e1d237b43..18981940a 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -702,6 +702,9 @@ async def lifespan(app: FastAPI): except Exception as e: log.warning(f"Failed to initialize tool/terminal servers at startup: {e}") + # Mark application as ready to accept traffic from a startup perspective. + app.state.startup_complete = True + yield if hasattr(app.state, "redis_task_command_listener"): @@ -716,6 +719,9 @@ app = FastAPI( lifespan=lifespan, ) +# Used by readiness checks to gate traffic until startup work is done. +app.state.startup_complete = False + # For Open WebUI OIDC/OAuth2 oauth_manager = OAuthManager(app) app.state.oauth_manager = oauth_manager @@ -2602,6 +2608,47 @@ async def healthcheck(): return {"status": True} +@app.get("/ready") +async def readiness_check(): + """ + Returns 200 only when the application is ready to accept traffic. + """ + + # Ensure application startup work has completed + if not getattr(app.state, "startup_complete", False): + log.info("Readiness check failed: startup not complete") + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Startup not complete", + ) + + # Check database connectivity + try: + ScopedSession.execute(text("SELECT 1;")).all() + except Exception as e: + log.warning(f"Readiness check DB ping failed: {e!r}") + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Database not ready", + ) + + # Check Redis connectivity if configured + redis = app.state.redis + if redis is not None: + try: + pong = await redis.ping() + if pong is False: + raise Exception("Redis PING returned False") + except Exception as e: + log.warning(f"Readiness check Redis ping failed: {e!r}") + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Redis not ready", + ) + + return {"status": True} + + @app.get("/health/db") async def healthcheck_with_db(): ScopedSession.execute(text("SELECT 1;")).all()