Skip to content

Commit 83154ad

Browse files
fix(api): handle non-json response (#1159)
1 parent 3d9adfb commit 83154ad

3 files changed

Lines changed: 83 additions & 20 deletions

File tree

plugins/api.ts

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { toast } from '@datagouv/components-next'
2+
import { getApiErrorMessage } from '~/utils/api'
23

34
export default defineNuxtPlugin({
45
async setup(nuxtApp) {
@@ -65,26 +66,7 @@ export default defineNuxtPlugin({
6566
return
6667
}
6768

68-
let message = ''
69-
if (response._data) {
70-
try {
71-
if ('error' in response._data) {
72-
message = response._data.error
73-
}
74-
else if ('message' in response._data) {
75-
message = response._data.message
76-
}
77-
else if ('errors' in response._data && typeof response._data.errors === 'object') {
78-
message = Object.entries(response._data.errors).map(([key, value]) => `${key}: ${value}`).join(' ; ')
79-
}
80-
else if ('response' in response._data && 'errors' in response._data.response && Array.isArray(response._data.response.errors)) {
81-
message = response._data.response.errors.join(' ; ')
82-
}
83-
}
84-
catch (e) {
85-
console.error(e)
86-
}
87-
}
69+
const message = getApiErrorMessage(response._data)
8870

8971
if (options?.method && ['POST', 'PUT', 'PATCH'].includes(options.method) && response.status === 400) {
9072
toast.error(t(`Le formulaire contient des erreurs. ${message}`))

tests-unit/utils/api.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
import { getApiErrorMessage } from '~/utils/api'
3+
4+
describe('getApiErrorMessage', () => {
5+
it('returns an empty string for string response data and logs a warning excerpt', () => {
6+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
7+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
8+
const html = '<html>Server Error</html>'
9+
10+
expect(getApiErrorMessage(html)).toBe('')
11+
expect(errorSpy).not.toHaveBeenCalled()
12+
expect(warnSpy).toHaveBeenCalledWith('[API] Non-JSON error response:', html)
13+
14+
errorSpy.mockRestore()
15+
warnSpy.mockRestore()
16+
})
17+
18+
it('returns an empty string for null or undefined response data', () => {
19+
expect(getApiErrorMessage(null)).toBe('')
20+
expect(getApiErrorMessage(undefined)).toBe('')
21+
})
22+
23+
it('extracts the `error` field when present', () => {
24+
expect(getApiErrorMessage({ error: 'Something went wrong' })).toBe('Something went wrong')
25+
})
26+
27+
it('extracts the `message` field when present', () => {
28+
expect(getApiErrorMessage({ message: 'A message' })).toBe('A message')
29+
})
30+
31+
it('prefers `error` over `message`', () => {
32+
expect(getApiErrorMessage({ error: 'First', message: 'Second' })).toBe('First')
33+
})
34+
35+
it('formats an `errors` object into a human readable string', () => {
36+
expect(getApiErrorMessage({ errors: { name: 'required', email: 'invalid' } })).toBe('name: required ; email: invalid')
37+
})
38+
39+
it('joins response.errors array when present', () => {
40+
expect(getApiErrorMessage({ response: { errors: ['bad', 'worse'] } })).toBe('bad ; worse')
41+
})
42+
})

utils/api.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,45 @@ export function getDataFromSSRPayload(key: string, nuxtApp: NuxtApp) {
6262
return nuxtApp.payload.data[key] ? nuxtApp.payload.data[key] : undefined
6363
}
6464

65+
/**
66+
* Extract a human-readable error message from an API error response body.
67+
* Handles non-JSON responses (e.g. HTML error pages) gracefully.
68+
*/
69+
export function getApiErrorMessage(data: unknown): string {
70+
if (!data || typeof data !== 'object') {
71+
if (typeof data === 'string' && data.length) {
72+
console.warn('[API] Non-JSON error response:', data.slice(0, 200))
73+
}
74+
return ''
75+
}
76+
77+
const record = data as Record<string, unknown>
78+
79+
if ('error' in record && typeof record.error === 'string') {
80+
return record.error
81+
}
82+
83+
if ('message' in record && typeof record.message === 'string') {
84+
return record.message
85+
}
86+
87+
if ('errors' in record && typeof record.errors === 'object' && record.errors !== null) {
88+
return Object.entries(record.errors).map(([key, value]) => `${key}: ${value}`).join(' ; ')
89+
}
90+
91+
if (
92+
'response' in record
93+
&& record.response
94+
&& typeof record.response === 'object'
95+
&& 'errors' in record.response
96+
&& Array.isArray((record.response as Record<string, unknown>).errors)
97+
) {
98+
return ((record.response as Record<string, unknown>).errors as string[]).join(' ; ')
99+
}
100+
101+
return ''
102+
}
103+
65104
export function usePostApiWithCsrf() {
66105
const { $api } = useNuxtApp()
67106

0 commit comments

Comments
 (0)