Spaces:
Build error
Build error
Claude commited on
fix: "error [object Object]" on API validation errors + SQLite migration
Browse filesTwo fixes:
1. Frontend: FastAPI returns validation errors as {"detail": [{loc, msg}...]}
(array of objects), not a string. The old code did `new Error(detail)` which
stringified the array as "[object Object]". New extractDetail() function
handles both string and array formats, producing readable messages like
"body β slug : field required".
2. Backend: add SQLite migration for the new supports_vision column on
model_configs. Since SQLAlchemy create_all doesn't ALTER existing tables,
existing databases on HuggingFace would crash. The migration runs at
startup and adds the column with DEFAULT 1 (true) if missing.
https://claude.ai/code/session_01Sxmf2zTTcjaQeZXCeXaCxr
- backend/app/main.py +16 -0
- frontend/src/lib/api.ts +26 -8
backend/app/main.py
CHANGED
|
@@ -23,6 +23,19 @@ from app.models.database import Base, engine
|
|
| 23 |
logger = logging.getLogger(__name__)
|
| 24 |
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
@asynccontextmanager
|
| 27 |
async def lifespan(application: FastAPI):
|
| 28 |
"""CrΓ©e les tables SQLite au dΓ©marrage, libΓ¨re l'engine Γ l'arrΓͺt."""
|
|
@@ -52,6 +65,9 @@ async def lifespan(application: FastAPI):
|
|
| 52 |
|
| 53 |
async with engine.begin() as conn:
|
| 54 |
await conn.run_sync(Base.metadata.create_all)
|
|
|
|
|
|
|
|
|
|
| 55 |
logger.info("Tables SQLite initialisΓ©es")
|
| 56 |
yield
|
| 57 |
await engine.dispose()
|
|
|
|
| 23 |
logger = logging.getLogger(__name__)
|
| 24 |
|
| 25 |
|
| 26 |
+
def _migrate_model_configs(connection) -> None:
|
| 27 |
+
"""Ajoute la colonne supports_vision si absente (migration BDD existantes)."""
|
| 28 |
+
from sqlalchemy import inspect, text
|
| 29 |
+
|
| 30 |
+
inspector = inspect(connection)
|
| 31 |
+
columns = {c["name"] for c in inspector.get_columns("model_configs")}
|
| 32 |
+
if "supports_vision" not in columns:
|
| 33 |
+
connection.execute(
|
| 34 |
+
text("ALTER TABLE model_configs ADD COLUMN supports_vision BOOLEAN NOT NULL DEFAULT 1")
|
| 35 |
+
)
|
| 36 |
+
logger.info("Migration : colonne supports_vision ajoutΓ©e Γ model_configs")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
@asynccontextmanager
|
| 40 |
async def lifespan(application: FastAPI):
|
| 41 |
"""CrΓ©e les tables SQLite au dΓ©marrage, libΓ¨re l'engine Γ l'arrΓͺt."""
|
|
|
|
| 65 |
|
| 66 |
async with engine.begin() as conn:
|
| 67 |
await conn.run_sync(Base.metadata.create_all)
|
| 68 |
+
# Migration : ajouter supports_vision aux model_configs existantes
|
| 69 |
+
# (create_all ne fait pas d'ALTER TABLE sur les tables existantes)
|
| 70 |
+
await conn.run_sync(_migrate_model_configs)
|
| 71 |
logger.info("Tables SQLite initialisΓ©es")
|
| 72 |
yield
|
| 73 |
await engine.dispose()
|
frontend/src/lib/api.ts
CHANGED
|
@@ -190,6 +190,28 @@ export interface CorpusProfile {
|
|
| 190 |
|
| 191 |
// ββ Fetch helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 192 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
async function get<T>(path: string): Promise<T> {
|
| 194 |
const resp = await fetch(`${BASE_URL}${path}`)
|
| 195 |
if (!resp.ok) throw new Error(`HTTP ${resp.status} β ${path}`)
|
|
@@ -204,8 +226,7 @@ async function post<T>(path: string, body?: unknown): Promise<T> {
|
|
| 204 |
})
|
| 205 |
if (!resp.ok) {
|
| 206 |
const payload = await resp.json().catch(() => null)
|
| 207 |
-
|
| 208 |
-
throw new Error(detail ?? `HTTP ${resp.status} β ${path}`)
|
| 209 |
}
|
| 210 |
return resp.json() as Promise<T>
|
| 211 |
}
|
|
@@ -218,8 +239,7 @@ async function put<T>(path: string, body?: unknown): Promise<T> {
|
|
| 218 |
})
|
| 219 |
if (!resp.ok) {
|
| 220 |
const payload = await resp.json().catch(() => null)
|
| 221 |
-
|
| 222 |
-
throw new Error(detail ?? `HTTP ${resp.status} β ${path}`)
|
| 223 |
}
|
| 224 |
return resp.json() as Promise<T>
|
| 225 |
}
|
|
@@ -228,8 +248,7 @@ async function del(path: string): Promise<void> {
|
|
| 228 |
const resp = await fetch(`${BASE_URL}${path}`, { method: 'DELETE' })
|
| 229 |
if (!resp.ok) {
|
| 230 |
const payload = await resp.json().catch(() => null)
|
| 231 |
-
|
| 232 |
-
throw new Error(detail ?? `HTTP ${resp.status} β ${path}`)
|
| 233 |
}
|
| 234 |
}
|
| 235 |
|
|
@@ -237,8 +256,7 @@ async function postForm<T>(path: string, data: FormData): Promise<T> {
|
|
| 237 |
const resp = await fetch(`${BASE_URL}${path}`, { method: 'POST', body: data })
|
| 238 |
if (!resp.ok) {
|
| 239 |
const payload = await resp.json().catch(() => null)
|
| 240 |
-
|
| 241 |
-
throw new Error(detail ?? `HTTP ${resp.status} β ${path}`)
|
| 242 |
}
|
| 243 |
return resp.json() as Promise<T>
|
| 244 |
}
|
|
|
|
| 190 |
|
| 191 |
// ββ Fetch helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 192 |
|
| 193 |
+
/**
|
| 194 |
+
* Extract a human-readable error message from a FastAPI error response.
|
| 195 |
+
* FastAPI may return { detail: "string" } or { detail: [{loc, msg, type}, ...] }
|
| 196 |
+
* for validation errors (422). This function handles both cases.
|
| 197 |
+
*/
|
| 198 |
+
function extractDetail(payload: unknown, fallback: string): string {
|
| 199 |
+
if (!payload || typeof payload !== 'object') return fallback
|
| 200 |
+
const detail = (payload as Record<string, unknown>).detail
|
| 201 |
+
if (typeof detail === 'string') return detail
|
| 202 |
+
if (Array.isArray(detail)) {
|
| 203 |
+
// FastAPI validation error: [{loc: [...], msg: "...", type: "..."}]
|
| 204 |
+
const messages = detail
|
| 205 |
+
.map((e: Record<string, unknown>) => {
|
| 206 |
+
const loc = Array.isArray(e.loc) ? e.loc.join(' β ') : ''
|
| 207 |
+
return loc ? `${loc} : ${e.msg}` : String(e.msg ?? '')
|
| 208 |
+
})
|
| 209 |
+
.filter(Boolean)
|
| 210 |
+
return messages.length > 0 ? messages.join(' ; ') : fallback
|
| 211 |
+
}
|
| 212 |
+
return fallback
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
async function get<T>(path: string): Promise<T> {
|
| 216 |
const resp = await fetch(`${BASE_URL}${path}`)
|
| 217 |
if (!resp.ok) throw new Error(`HTTP ${resp.status} β ${path}`)
|
|
|
|
| 226 |
})
|
| 227 |
if (!resp.ok) {
|
| 228 |
const payload = await resp.json().catch(() => null)
|
| 229 |
+
throw new Error(extractDetail(payload, `HTTP ${resp.status} β ${path}`))
|
|
|
|
| 230 |
}
|
| 231 |
return resp.json() as Promise<T>
|
| 232 |
}
|
|
|
|
| 239 |
})
|
| 240 |
if (!resp.ok) {
|
| 241 |
const payload = await resp.json().catch(() => null)
|
| 242 |
+
throw new Error(extractDetail(payload, `HTTP ${resp.status} β ${path}`))
|
|
|
|
| 243 |
}
|
| 244 |
return resp.json() as Promise<T>
|
| 245 |
}
|
|
|
|
| 248 |
const resp = await fetch(`${BASE_URL}${path}`, { method: 'DELETE' })
|
| 249 |
if (!resp.ok) {
|
| 250 |
const payload = await resp.json().catch(() => null)
|
| 251 |
+
throw new Error(extractDetail(payload, `HTTP ${resp.status} β ${path}`))
|
|
|
|
| 252 |
}
|
| 253 |
}
|
| 254 |
|
|
|
|
| 256 |
const resp = await fetch(`${BASE_URL}${path}`, { method: 'POST', body: data })
|
| 257 |
if (!resp.ok) {
|
| 258 |
const payload = await resp.json().catch(() => null)
|
| 259 |
+
throw new Error(extractDetail(payload, `HTTP ${resp.status} β ${path}`))
|
|
|
|
| 260 |
}
|
| 261 |
return resp.json() as Promise<T>
|
| 262 |
}
|