From 3dad5c6088eb54dc04c1495763c1543b3a821325 Mon Sep 17 00:00:00 2001 From: Arthur Ria Date: Mon, 24 Aug 2026 16:41:18 +0200 Subject: [PATCH] =?UTF-8?q?L1.1=20:=20remonte=20le=20d=C3=A9tail=20des=20e?= =?UTF-8?q?rreurs=20HTTP=20dans=20les=20r=C3=A9ponses=20d'outils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Toute erreur d'API se résumait à « Request failed with status code 500 » alors que le WMS renvoie le diagnostic complet (erreurs de compilation LINQ, entité inconnue…) dans le corps de la réponse, jusqu'ici jeté par les catch de post() et get(). L'erreur propagée porte désormais : verbe, URL complète, statut HTTP, payload envoyé (dont Application et QueryType), et corps de réponse tronqué à 2000 caractères. Pour les corps structurés, Message et InnerException.Message sont extraits plutôt qu'un JSON.stringify intégral qui noierait le diagnostic dans le bruit WatsonBuckets. Le rejeu après refresh de token 401 est conservé, et une erreur pendant le rejeu est enrichie de la même façon. Aucun credential ni token dans le message (les headers ne sont jamais inclus). Vérifié contre le WMS réel : query_wms_entities("Container") fait apparaître « 'ApplicationReadingContext' ne contient pas de définition pour 'Container' » dans la réponse de l'outil. npm test : 4/4. Co-Authored-By: Claude Opus 5 --- src/services/api-service.js | 121 +++++++++++++++++++++++++++++------- 1 file changed, 99 insertions(+), 22 deletions(-) diff --git a/src/services/api-service.js b/src/services/api-service.js index fde5356..adf9912 100644 --- a/src/services/api-service.js +++ b/src/services/api-service.js @@ -136,6 +136,71 @@ class APIService { } } + /** + * Extract the useful part of an HTTP error response body. + * Structured WMS errors carry the diagnostic in Message / InnerException.Message — + * a full JSON.stringify would drown it in WatsonBuckets / HResult noise. + * @param {*} data - Response body (object, string, or anything axios parsed) + * @returns {string|null} Truncated human-readable body, or null if empty + */ + _describeResponseBody(data) { + const MAX_BODY_LENGTH = 2000; + if (data == null || data === '') return null; + if (typeof data === 'string') return data.slice(0, MAX_BODY_LENGTH); + if (typeof data === 'object') { + const parts = []; + if (data.ClassName) parts.push(data.ClassName); + if (data.Message) parts.push(data.Message); + let inner = data.InnerException; + while (inner && inner.Message) { + // AggregateException répète souvent le même message dans InnerException + if (inner.Message !== data.Message) parts.push(`Inner: ${inner.Message}`); + inner = inner.InnerException; + } + const text = parts.length > 0 ? parts.join(' — ') : JSON.stringify(data); + return text.slice(0, MAX_BODY_LENGTH); + } + return String(data).slice(0, MAX_BODY_LENGTH); + } + + /** + * Build an enriched Error from a failed HTTP call: status, verb, full URL, + * request payload and response body. The WMS puts the real diagnostic + * (compile errors, unknown entity, ...) in the response body — without this, + * every failure reads "Request failed with status code 500". + * Never includes headers (Bearer token) — payloads passed through post/get + * carry no credentials. + * @param {Error} error - Original axios error + * @param {string} method - HTTP verb ('POST' | 'GET') + * @param {string} url - Full request URL + * @param {*} payload - Request body (POST) or query params (GET) + * @returns {Error} Enriched error (original kept in .cause, status in .status) + */ + _enrichHttpError(error, method, url, payload) { + const status = error.response?.status; + const parts = [`${method} ${url} failed${status != null ? ` (HTTP ${status})` : ''}: ${error.message}`]; + + if (payload !== undefined && payload !== null) { + let serialized; + try { + serialized = JSON.stringify(payload); + } catch { + serialized = String(payload); + } + if (serialized !== '{}') { + parts.push(`Request payload: ${serialized.slice(0, 1000)}`); + } + } + + const body = this._describeResponseBody(error.response?.data); + if (body) parts.push(`Response body: ${body}`); + + const enriched = new Error(parts.join('\n')); + enriched.status = status; + enriched.cause = error; + return enriched; + } + /** * Make a POST request to WMS API * @param {string} endpoint - API endpoint (e.g., '/QueryExecute' or '/AD/api/Workflow/GetByApplication') @@ -162,25 +227,31 @@ class APIService { return response.data; } catch (error) { - console.error(`[API] Request failed: ${error.message}`); - // If unauthorized, try refreshing token and retry once if (error.response?.status === 401) { console.error('[API] Unauthorized, refreshing token and retrying...'); await this.refreshOAuthToken(); - const retryResponse = await this.httpClient.post(url, data, { - headers: { - 'Authorization': `Bearer ${this.token}`, - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - }); + try { + const retryResponse = await this.httpClient.post(url, data, { + headers: { + 'Authorization': `Bearer ${this.token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); - return retryResponse.data; + return retryResponse.data; + } catch (retryError) { + const enrichedRetry = this._enrichHttpError(retryError, 'POST', url, data); + console.error(`[API] Retry after token refresh failed: ${enrichedRetry.message}`); + throw enrichedRetry; + } } - throw error; + const enriched = this._enrichHttpError(error, 'POST', url, data); + console.error(`[API] Request failed: ${enriched.message}`); + throw enriched; } } @@ -210,25 +281,31 @@ class APIService { return response.data; } catch (error) { - console.error(`[API] Request failed: ${error.message}`); - // If unauthorized, try refreshing token and retry once if (error.response?.status === 401) { console.error('[API] Unauthorized, refreshing token and retrying...'); await this.refreshOAuthToken(); - const retryResponse = await this.httpClient.get(url, { - params, - headers: { - 'Authorization': `Bearer ${this.token}`, - 'Accept': 'application/json' - } - }); + try { + const retryResponse = await this.httpClient.get(url, { + params, + headers: { + 'Authorization': `Bearer ${this.token}`, + 'Accept': 'application/json' + } + }); - return retryResponse.data; + return retryResponse.data; + } catch (retryError) { + const enrichedRetry = this._enrichHttpError(retryError, 'GET', url, params); + console.error(`[API] Retry after token refresh failed: ${enrichedRetry.message}`); + throw enrichedRetry; + } } - throw error; + const enriched = this._enrichHttpError(error, 'GET', url, params); + console.error(`[API] Request failed: ${enriched.message}`); + throw enriched; } }