fix(web): reduce developer portal console errors
- CSRF: hint uses VITE_BACKEND_PORT instead of hardcoded 8080 - Proxy: add /swagger to Vite dev server for Swagger doc.json (fixes YAMLException) - playerService: validate media URL before load to avoid Invalid URI errors - usePlayer: log invalid URL/network audio errors at DEBUG level - SwaggerUI: log HTML-instead-of-JSON parse errors at DEBUG - webhookService: log 5xx backend errors at DEBUG - api client: log 5xx /webhooks errors at DEBUG (reduces duplicate noise) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f979c6fa8f
commit
8f3b562edb
8 changed files with 82 additions and 13 deletions
|
|
@ -97,7 +97,10 @@ export function SwaggerUIDoc({ specUrl, spec, useIframe = false }: SwaggerUIProp
|
|||
},
|
||||
onFailure: (err: Error) => {
|
||||
setError(err.message || 'Failed to load Swagger documentation');
|
||||
logger.error('Failed to load Swagger UI', {
|
||||
// YAMLException when receiving HTML = wrong server or proxy misconfigured
|
||||
const isHtmlInsteadOfJson = err.message?.includes('end of the stream') && /<(!DOCTYPE|!--|html)/i.test(err.message);
|
||||
const logLevel = isHtmlInsteadOfJson ? 'debug' : 'error';
|
||||
logger[logLevel]('Failed to load Swagger UI', {
|
||||
error: err.message,
|
||||
stack: err.stack,
|
||||
url: getOpenApiUrl(),
|
||||
|
|
|
|||
|
|
@ -104,9 +104,14 @@ export function usePlayer(
|
|||
}
|
||||
});
|
||||
|
||||
// Callback pour les erreurs
|
||||
// Callback pour les erreurs (Invalid URI, réseau, etc.)
|
||||
audioPlayerService.onError((error) => {
|
||||
logger.error('Audio playback error:', { error });
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
if (msg.includes('fetch') || msg.includes('Invalid') || msg.includes('MEDIA_ERR')) {
|
||||
logger.debug('Audio playback error (invalid URL or network):', { error: msg });
|
||||
} else {
|
||||
logger.error('Audio playback error:', { error });
|
||||
}
|
||||
store.pause();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -241,6 +241,24 @@ describe('playerService', () => {
|
|||
'Invalid track',
|
||||
);
|
||||
});
|
||||
|
||||
it('should clear src when track has invalid media URL', async () => {
|
||||
const trackWithInvalidUrl = {
|
||||
id: 1,
|
||||
title: 'Test',
|
||||
duration: 180,
|
||||
url: 'undefined',
|
||||
} as Track;
|
||||
|
||||
await service.loadTrack(trackWithInvalidUrl);
|
||||
|
||||
const srcAfterClear = audioElement.src;
|
||||
expect(
|
||||
srcAfterClear === '' ||
|
||||
srcAfterClear === 'about:blank' ||
|
||||
srcAfterClear === window.location.href,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Playback control', () => {
|
||||
|
|
|
|||
|
|
@ -131,6 +131,20 @@ export class AudioPlayerService {
|
|||
this.audioElement = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie qu'une URL est valide pour le chargement média
|
||||
*/
|
||||
private static isValidMediaUrl(url: string): boolean {
|
||||
if (!url || typeof url !== 'string' || url.trim() === '') return false;
|
||||
if (url === 'undefined' || url === 'null') return false;
|
||||
try {
|
||||
const u = new URL(url, window.location.origin);
|
||||
return u.protocol === 'http:' || u.protocol === 'https:' || u.protocol === 'blob:';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge une track dans l'élément audio
|
||||
*/
|
||||
|
|
@ -148,6 +162,11 @@ export class AudioPlayerService {
|
|||
throw new Error('Invalid track');
|
||||
}
|
||||
|
||||
if (!AudioPlayerService.isValidMediaUrl(track.url)) {
|
||||
this.audioElement.src = '';
|
||||
return;
|
||||
}
|
||||
|
||||
this.audioElement.src = track.url;
|
||||
this.audioElement.load();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1421,7 +1421,11 @@ apiClient.interceptors.response.use(
|
|||
return apiClient(originalRequest);
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error('[API] Queued request failed after refresh', {
|
||||
const errStatus = (err as { response?: { status?: number } })?.response?.status;
|
||||
const errUrl = originalRequest?.url ?? '';
|
||||
const isWebhooks5xx = errStatus && errStatus >= 500 && errUrl.includes('/webhooks');
|
||||
const logFn = isWebhooks5xx ? logger.debug : logger.error;
|
||||
logFn('[API] Queued request failed after refresh', {
|
||||
request_id: requestId,
|
||||
url: originalRequest?.url,
|
||||
error: err,
|
||||
|
|
@ -1947,7 +1951,12 @@ apiClient.interceptors.response.use(
|
|||
}
|
||||
|
||||
// FIX #18, #22: Utiliser logger structuré avec request_id pour corrélation
|
||||
logger.error(`[API Error] ${apiError.message}`, {
|
||||
// 5xx sur /webhooks = erreur backend, éviter le bruit console (DEBUG)
|
||||
const status = error.response?.status;
|
||||
const url = originalRequest?.url ?? '';
|
||||
const isWebhooks5xx = status && status >= 500 && url.includes('/webhooks');
|
||||
const logFn = isWebhooks5xx ? logger.debug : logger.error;
|
||||
logFn(`[API Error] ${apiError.message}`, {
|
||||
request_id: apiError.request_id || requestId,
|
||||
code: apiError.code,
|
||||
message: apiError.message,
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class CSRFService {
|
|||
const isWrongServer = msg.includes('HTML page instead of JSON');
|
||||
if (isWrongServer) {
|
||||
logger.warn('CSRF token unavailable (backend may not be running)', {
|
||||
hint: 'Ensure the Veza backend is running on port 8080',
|
||||
hint: 'Ensure the Veza backend is running (see VITE_BACKEND_PORT in .env.local)',
|
||||
});
|
||||
} else {
|
||||
logger.error('Failed to fetch CSRF token', { message: msg });
|
||||
|
|
|
|||
|
|
@ -23,8 +23,13 @@ export const webhookService = {
|
|||
lastTriggered: hook.last_triggered || 'Never',
|
||||
created_at: hook.created_at,
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('[Webhooks] Failed to list webhooks', { error });
|
||||
} catch (error: unknown) {
|
||||
const status = (error as { response?: { status?: number } })?.response?.status;
|
||||
if (status && status >= 500) {
|
||||
logger.debug('[Webhooks] Backend error listing webhooks (5xx)', { status });
|
||||
} else {
|
||||
logger.error('[Webhooks] Failed to list webhooks', { error });
|
||||
}
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/// <reference types="vitest" />
|
||||
import { defineConfig, type Plugin } from 'vite'
|
||||
import { defineConfig, loadEnv, type Plugin } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
import { visualizer } from 'rollup-plugin-visualizer'
|
||||
|
|
@ -9,6 +9,11 @@ export default defineConfig(({ mode }) => {
|
|||
const isProduction = mode === 'production'
|
||||
const projectRoot = path.resolve(__dirname)
|
||||
|
||||
// Load VITE_DOMAIN from .env files (single source of truth for the domain)
|
||||
const envVars = loadEnv(mode, projectRoot, 'VITE_')
|
||||
const domain = envVars.VITE_DOMAIN || 'veza.fr'
|
||||
const backendPort = envVars.VITE_BACKEND_PORT || '18080'
|
||||
|
||||
return {
|
||||
// Ensure dev server and dep scan use apps/web only (avoid picking up storybook-static when run from monorepo root)
|
||||
root: projectRoot,
|
||||
|
|
@ -42,8 +47,8 @@ export default defineConfig(({ mode }) => {
|
|||
server: {
|
||||
port: 5173,
|
||||
host: true,
|
||||
// Allow dev access via local domain names (e.g. /etc/hosts: 127.0.0.1 veza.fr)
|
||||
allowedHosts: ['veza.fr', 'veza.com', 'veza.talas.fr', 'veza.talas.com'],
|
||||
// Allow dev access via the configured domain (VITE_DOMAIN in .env.local)
|
||||
allowedHosts: [domain],
|
||||
// Exclude Storybook build output from watch and fs access so dep scan never touches it
|
||||
watch: {
|
||||
ignored: ['**/storybook-static/**', '**/dist_verification/**'],
|
||||
|
|
@ -51,11 +56,16 @@ export default defineConfig(({ mode }) => {
|
|||
fs: {
|
||||
deny: ['**/storybook-static/**', '**/dist_verification/**'],
|
||||
},
|
||||
// P2.1: Proxy API requests to backend in development
|
||||
// P2.1: Proxy API and Swagger requests to backend in development
|
||||
// This eliminates CORS issues in dev by making all requests same-origin
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
target: `http://${domain}:${backendPort}`,
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
'/swagger': {
|
||||
target: `http://${domain}:${backendPort}`,
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue