Claude commited on
Commit
f8ed5d0
Β·
unverified Β·
1 Parent(s): b0cf89a

fix: "error [object Object]" on API validation errors + SQLite migration

Browse files

Two 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

Files changed (2) hide show
  1. backend/app/main.py +16 -0
  2. 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
- const detail = (payload as { detail?: string } | null)?.detail
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
- const detail = (payload as { detail?: string } | null)?.detail
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
- const detail = (payload as { detail?: string } | null)?.detail
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
- const detail = (payload as { detail?: string } | null)?.detail
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
  }