L1.1 : remonte le détail des erreurs HTTP dans les réponses d'outils

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 <noreply@anthropic.com>
This commit is contained in:
Arthur Ria
2026-08-24 16:41:18 +02:00
parent 7c722dae91
commit 3dad5c6088
+99 -22
View File
@@ -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 * Make a POST request to WMS API
* @param {string} endpoint - API endpoint (e.g., '/QueryExecute' or '/AD/api/Workflow/GetByApplication') * @param {string} endpoint - API endpoint (e.g., '/QueryExecute' or '/AD/api/Workflow/GetByApplication')
@@ -162,25 +227,31 @@ class APIService {
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error(`[API] Request failed: ${error.message}`);
// If unauthorized, try refreshing token and retry once // If unauthorized, try refreshing token and retry once
if (error.response?.status === 401) { if (error.response?.status === 401) {
console.error('[API] Unauthorized, refreshing token and retrying...'); console.error('[API] Unauthorized, refreshing token and retrying...');
await this.refreshOAuthToken(); await this.refreshOAuthToken();
const retryResponse = await this.httpClient.post(url, data, { try {
headers: { const retryResponse = await this.httpClient.post(url, data, {
'Authorization': `Bearer ${this.token}`, headers: {
'Content-Type': 'application/json', 'Authorization': `Bearer ${this.token}`,
'Accept': 'application/json' '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; return response.data;
} catch (error) { } catch (error) {
console.error(`[API] Request failed: ${error.message}`);
// If unauthorized, try refreshing token and retry once // If unauthorized, try refreshing token and retry once
if (error.response?.status === 401) { if (error.response?.status === 401) {
console.error('[API] Unauthorized, refreshing token and retrying...'); console.error('[API] Unauthorized, refreshing token and retrying...');
await this.refreshOAuthToken(); await this.refreshOAuthToken();
const retryResponse = await this.httpClient.get(url, { try {
params, const retryResponse = await this.httpClient.get(url, {
headers: { params,
'Authorization': `Bearer ${this.token}`, headers: {
'Accept': 'application/json' '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;
} }
} }