fix: renvoie un 404 sur une URL d'API inconnue
L'API et l'interface partagent le même port, et la route attrape-tout qui sert le SPA passait avant le 404 : n'importe quelle URL /api/* erronée repartait en index.html avec un 200. Côté client, une faute de frappe dans une URL ne ressemblait pas à une erreur mais à une réponse vide, et le vrai motif n'apparaissait qu'en inspectant le corps de la réponse. Les URL commençant par /api/ sont désormais exclues de l'attrape-tout et retombent sur un 404 JSON. Les routes de l'interface restent servies par index.html. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI, HTTPException
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
@@ -87,6 +87,13 @@ if FRONTEND_DIST.exists():
|
|||||||
@app.get("/{full_path:path}", include_in_schema=False)
|
@app.get("/{full_path:path}", include_in_schema=False)
|
||||||
async def serve_spa(full_path: str):
|
async def serve_spa(full_path: str):
|
||||||
"""Serve the SPA for all non-API routes."""
|
"""Serve the SPA for all non-API routes."""
|
||||||
|
# Une URL d'API inconnue doit se dire inconnue. Sans cette garde, elle
|
||||||
|
# tomberait dans le catch-all et repartirait en index.html avec un 200 :
|
||||||
|
# côté client, une faute de frappe dans une URL ne ressemblerait plus à
|
||||||
|
# une erreur mais à une réponse vide.
|
||||||
|
if full_path == "api" or full_path.startswith("api/"):
|
||||||
|
raise HTTPException(status_code=404, detail="Endpoint inconnu")
|
||||||
|
|
||||||
# If requesting a file that exists (and stays within dist), serve it
|
# If requesting a file that exists (and stays within dist), serve it
|
||||||
file_path = (FRONTEND_DIST / full_path).resolve()
|
file_path = (FRONTEND_DIST / full_path).resolve()
|
||||||
if file_path.is_file() and file_path.is_relative_to(_dist_root):
|
if file_path.is_file() and file_path.is_relative_to(_dist_root):
|
||||||
|
|||||||
41
tests/test_routage_spa.py
Normal file
41
tests/test_routage_spa.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
"""Tests du partage des URL entre l'API et le SPA.
|
||||||
|
|
||||||
|
L'application sert l'API et l'interface sur le meme port : une route attrape
|
||||||
|
tout ce qui ne correspond a aucun endpoint pour le renvoyer au routeur Vue. Elle
|
||||||
|
ne doit pas avaler les URL d'API, sous peine de transformer une adresse erronee
|
||||||
|
en page HTML repondue avec un 200.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from plesna_gerance.api.app import FRONTEND_DIST
|
||||||
|
|
||||||
|
|
||||||
|
def test_une_url_d_api_inconnue_donne_404(api_client):
|
||||||
|
response = api_client.get("/api/nexiste-pas")
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
assert response.headers["content-type"].startswith("application/json")
|
||||||
|
|
||||||
|
|
||||||
|
def test_une_url_d_api_inconnue_ne_renvoie_pas_le_spa(api_client):
|
||||||
|
"""Meme forme que les vraies routes : le prefixe seul ne suffit pas."""
|
||||||
|
response = api_client.get("/api/revenus/inconnu")
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_les_routes_d_api_existantes_repondent(api_client):
|
||||||
|
assert api_client.get("/api/health").status_code == 200
|
||||||
|
assert api_client.get("/api/tags").status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
not FRONTEND_DIST.exists(), reason="frontend non construit (frontend/dist)"
|
||||||
|
)
|
||||||
|
def test_une_route_du_spa_renvoie_l_interface(api_client):
|
||||||
|
"""Les URL de l'interface restent servies par index.html."""
|
||||||
|
response = api_client.get("/documents")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.headers["content-type"].startswith("text/html")
|
||||||
Reference in New Issue
Block a user