KhaledSalehKL1 commited on
Commit
ea0995f
·
verified ·
1 Parent(s): 82d79e8

Serve React SPA at root so /assets/* resolve (fix white screen)

Browse files
Files changed (1) hide show
  1. app.py +21 -21
app.py CHANGED
@@ -4,8 +4,13 @@ The Space uses the Docker SDK, so we serve everything from one plain FastAPI
4
  app on port 7860:
5
 
6
  * ``/api/v1/*`` -> the real 9XAIPal backend router
7
- * ``/ui/*`` -> the compiled React build (SPA, with client-side routing)
8
- * ``/`` -> redirects to ``/ui/``
 
 
 
 
 
9
 
10
  We deliberately do NOT use Gradio as a wrapper: as of Gradio 6.0 the FastAPI
11
  instance exposed as ``demo.app`` is no longer the app that ``launch()`` serves,
@@ -20,7 +25,6 @@ from pathlib import Path
20
 
21
  import uvicorn
22
  from fastapi import FastAPI
23
- from fastapi.responses import RedirectResponse
24
  from fastapi.staticfiles import StaticFiles
25
  from starlette.responses import FileResponse
26
 
@@ -73,9 +77,17 @@ except Exception as exc: # pragma: no cover
73
  logger.warning("Backend package not importable (%s); serving static UI only", exc)
74
 
75
 
 
 
 
 
 
 
76
  # -----------------------------------------------------------------------------
77
- # Serve the compiled React build under /ui with SPA fallback so client-side
78
- # routes (deep links) resolve to index.html instead of 404.
 
 
79
  # -----------------------------------------------------------------------------
80
  _index = UI_DIR / "index.html"
81
  if UI_DIR.is_dir() and _index.exists():
@@ -84,35 +96,23 @@ if UI_DIR.is_dir() and _index.exists():
84
  async def get_response(self, path, scope):
85
  response = await super().get_response(path, scope)
86
  if response.status_code == 404:
87
- # Unknown sub-path: hand it back to the SPA router.
88
  return FileResponse(_index)
89
  return response
90
 
91
  app.mount(
92
- "/ui",
93
  SPAStaticFiles(directory=str(UI_DIR), html=True, check_dir=False),
94
  name="ui",
95
  )
96
- logger.info("React UI mounted at /ui (source: %s)", UI_DIR)
97
  else:
98
  logger.warning(
99
- "No compiled React build found at %s; /ui will be unavailable. "
100
  "Run `npm run build` in the frontend and copy `dist/` here.",
101
  UI_DIR,
102
  )
103
 
104
 
105
- @app.get("/", include_in_schema=False)
106
- def root() -> RedirectResponse:
107
- """Send visitors from the Space root to the React UI."""
108
- return RedirectResponse(url="/ui/")
109
-
110
-
111
- @app.get("/healthz", include_in_schema=False)
112
- def healthz() -> dict:
113
- """Lightweight liveness probe for the container itself."""
114
- return {"status": "ok"}
115
-
116
-
117
  if __name__ == "__main__":
118
  uvicorn.run(app, host=HOST, port=PORT, log_level="info")
 
4
  app on port 7860:
5
 
6
  * ``/api/v1/*`` -> the real 9XAIPal backend router
7
+ * ``/*`` -> the compiled React build (SPA, with client-side routing)
8
+
9
+ The React build (``dist/index.html``) references its bundles with absolute
10
+ root paths (``/assets/index-*.js``), so the SPA MUST be served from the root.
11
+ Mounting it under a sub-path (e.g. ``/ui``) makes those asset requests 404 and
12
+ the page renders blank. The API router is registered before the static mount,
13
+ so ``/api/v1`` and ``/api/docs`` still win over the catch-all.
14
 
15
  We deliberately do NOT use Gradio as a wrapper: as of Gradio 6.0 the FastAPI
16
  instance exposed as ``demo.app`` is no longer the app that ``launch()`` serves,
 
25
 
26
  import uvicorn
27
  from fastapi import FastAPI
 
28
  from fastapi.staticfiles import StaticFiles
29
  from starlette.responses import FileResponse
30
 
 
77
  logger.warning("Backend package not importable (%s); serving static UI only", exc)
78
 
79
 
80
+ @app.get("/healthz", include_in_schema=False)
81
+ def healthz() -> dict:
82
+ """Lightweight liveness probe for the container itself."""
83
+ return {"status": "ok"}
84
+
85
+
86
  # -----------------------------------------------------------------------------
87
+ # Serve the compiled React build at the ROOT with SPA fallback so client-side
88
+ # routes (deep links) resolve to index.html instead of 404. This MUST be the
89
+ # last route added: as a "/" mount it matches every path, so the API router and
90
+ # /healthz above (registered first) take precedence.
91
  # -----------------------------------------------------------------------------
92
  _index = UI_DIR / "index.html"
93
  if UI_DIR.is_dir() and _index.exists():
 
96
  async def get_response(self, path, scope):
97
  response = await super().get_response(path, scope)
98
  if response.status_code == 404:
99
+ # Unknown client-side route: hand it back to the SPA router.
100
  return FileResponse(_index)
101
  return response
102
 
103
  app.mount(
104
+ "/",
105
  SPAStaticFiles(directory=str(UI_DIR), html=True, check_dir=False),
106
  name="ui",
107
  )
108
+ logger.info("React UI mounted at / (source: %s)", UI_DIR)
109
  else:
110
  logger.warning(
111
+ "No compiled React build found at %s; the UI will be unavailable. "
112
  "Run `npm run build` in the frontend and copy `dist/` here.",
113
  UI_DIR,
114
  )
115
 
116
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  if __name__ == "__main__":
118
  uvicorn.run(app, host=HOST, port=PORT, log_level="info")