¡Buenos días, dev community! ![]()
Los martes nos enfocamos en resolver problemas reales. Hoy analizamos los errores más frustrantes en integraciones de APIs y las técnicas sistemáticas para debuggearlos antes de que lleguen a producción.
Problema #1: CORS Errors - El Terror del Frontend
Síntomas:
-
Access-Control-Allow-Originerrors en consola -
Requests que funcionan en Postman pero fallan en browser
-
Headers personalizados que desaparecen misteriosamente
Código Problemático:
// ❌ Headers que causan preflight request
fetch('https://api.external.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'value', // Esto causa preflight
'Authorization': 'Bearer token'
},
body: JSON.stringify(data)
});
Debugging Sistemático:
// Paso 1: Verificar si es preflight request
const corsDebugger = {
simpleRequest: false,
preflightRequired: false,
analyzeRequest(method, headers) {
// Simple request criteria
const simpleMethods = ['GET', 'HEAD', 'POST'];
const simpleHeaders = [
'accept', 'accept-language', 'content-language',
'content-type' // Solo para valores específicos
];
this.simpleRequest = simpleMethods.includes(method) &&
Object.keys(headers).every(header =>
simpleHeaders.includes(header.toLowerCase())
);
this.preflightRequired = !this.simpleRequest;
console.log('🔍 CORS Analysis:', {
method,
headers,
simpleRequest: this.simpleRequest,
preflightRequired: this.preflightRequired
});
}
};
// Paso 2: Implementar fallback strategy
async function robustApiCall(url, options = {}) {
corsDebugger.analyzeRequest(options.method || 'GET', options.headers || {});
try {
const response = await fetch(url, options);
return response;
} catch (error) {
if (error.message.includes('CORS')) {
console.error('🚨 CORS Error detectado:', error);
// Estrategia fallback: usar proxy o servidor
const proxyUrl = `/api/proxy?url=${encodeURIComponent(url)}`;
return fetch(proxyUrl, {
...options,
headers: {
'X-Proxy-Headers': JSON.stringify(options.headers)
}
});
}
throw error;
}
}
Soluciones Prácticas:
// Backend proxy para desarrollo
app.use('/api/proxy', async (req, res) => {
const targetUrl = req.query.url;
const proxyHeaders = req.headers['x-proxy-headers']
? JSON.parse(req.headers['x-proxy-headers'])
: {};
try {
const response = await fetch(targetUrl, {
method: req.method,
headers: proxyHeaders,
body: req.method !== 'GET' ? JSON.stringify(req.body) : undefined
});
const data = await response.json();
res.json(data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Problema #2: Timeouts y Rate Limiting Impredecibles
Síntomas:
-
Requests que funcionan a veces y fallan otras
-
429 (Too Many Requests) errors esporádicos
-
Aplicación que se cuelga esperando respuestas
Implementar Rate Limiting Inteligente:
class RateLimitedApiClient {
constructor(baseUrl, maxRequestsPerSecond = 10) {
this.baseUrl = baseUrl;
this.requestQueue = [];
this.isProcessing = false;
this.requestInterval = 1000 / maxRequestsPerSecond;
this.lastRequestTime = 0;
this.retryDelays = [1000, 2000, 4000, 8000]; // Exponential backoff
}
async request(endpoint, options = {}) {
return new Promise((resolve, reject) => {
this.requestQueue.push({
endpoint,
options: {
timeout: 10000, // Default timeout
retries: 3,
...options
},
resolve,
reject,
attempt: 0
});
this.processQueue();
});
}
async processQueue() {
if (this.isProcessing || this.requestQueue.length === 0) return;
this.isProcessing = true;
while (this.requestQueue.length > 0) {
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequestTime;
if (timeSinceLastRequest < this.requestInterval) {
await this.sleep(this.requestInterval - timeSinceLastRequest);
}
const request = this.requestQueue.shift();
await this.executeRequest(request);
this.lastRequestTime = Date.now();
}
this.isProcessing = false;
}
async executeRequest(request) {
const { endpoint, options, resolve, reject, attempt } = request;
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, options.timeout);
const response = await fetch(`${this.baseUrl}${endpoint}`, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
if (response.status === 429) {
// Rate limited - retry with exponential backoff
if (attempt < options.retries) {
const delay = this.retryDelays[attempt] || 8000;
console.log(`⏳ Rate limited. Retrying in ${delay}ms...`);
setTimeout(() => {
this.requestQueue.unshift({
...request,
attempt: attempt + 1
});
this.processQueue();
}, delay);
return;
}
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
resolve(data);
} catch (error) {
if (error.name === 'AbortError') {
reject(new Error(`Request timeout after ${options.timeout}ms`));
} else if (attempt < options.retries) {
// Retry para otros errores
setTimeout(() => {
this.requestQueue.unshift({
...request,
attempt: attempt + 1
});
this.processQueue();
}, this.retryDelays[attempt] || 1000);
} else {
reject(error);
}
}
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Uso
const apiClient = new RateLimitedApiClient('https://api.example.com', 5);
const data = await apiClient.request('/users/123');
Problema #3: Authentication Token Hell
Síntomas:
-
401 Unauthorized errors intermitentes
-
Tokens que expiran en medio de operaciones
-
Refresh token loops infinitos
Token Manager Robusto:
class TokenManager {
constructor(authConfig) {
this.accessToken = null;
this.refreshToken = null;
this.tokenExpiry = null;
this.refreshPromise = null;
this.authConfig = authConfig;
}
async getValidToken() {
// Si tenemos token y no ha expirado
if (this.accessToken && this.tokenExpiry > Date.now() + 60000) {
return this.accessToken;
}
// Si ya hay un refresh en progreso, esperar
if (this.refreshPromise) {
return this.refreshPromise;
}
// Iniciar refresh
this.refreshPromise = this.refreshTokens();
try {
const token = await this.refreshPromise;
return token;
} finally {
this.refreshPromise = null;
}
}
async refreshTokens() {
console.log('🔄 Refreshing authentication tokens...');
try {
const response = await fetch(`${this.authConfig.baseUrl}/auth/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
refresh_token: this.refreshToken
})
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
const tokenData = await response.json();
this.accessToken = tokenData.access_token;
this.refreshToken = tokenData.refresh_token;
this.tokenExpiry = Date.now() + (tokenData.expires_in * 1000);
console.log('✅ Tokens refreshed successfully');
return this.accessToken;
} catch (error) {
console.error('❌ Token refresh failed:', error);
// Redirect to login
this.clearTokens();
window.location.href = '/login';
throw error;
}
}
clearTokens() {
this.accessToken = null;
this.refreshToken = null;
this.tokenExpiry = null;
}
}
// Interceptor para requests automáticos
class AuthenticatedApiClient {
constructor(tokenManager) {
this.tokenManager = tokenManager;
}
async request(url, options = {}) {
const token = await this.tokenManager.getValidToken();
const authenticatedOptions = {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`
}
};
const response = await fetch(url, authenticatedOptions);
// Si aún así recibimos 401, intentar refresh una vez más
if (response.status === 401) {
console.log('🔄 Received 401, attempting token refresh...');
const newToken = await this.tokenManager.refreshTokens();
return fetch(url, {
...authenticatedOptions,
headers: {
...authenticatedOptions.headers,
'Authorization': `Bearer ${newToken}`
}
});
}
return response;
}
}
Problema #4: Response Format Inconsistencies
Síntomas:
-
APIs que a veces retornan arrays, a veces objetos
-
Campos que aparecen y desaparecen
-
Estructuras de error inconsistentes
Response Normalizer:
class ApiResponseNormalizer {
constructor() {
this.schemas = new Map();
}
// Registrar schema esperado para endpoint
registerSchema(endpoint, schema) {
this.schemas.set(endpoint, schema);
}
async normalizeResponse(endpoint, rawResponse) {
const schema = this.schemas.get(endpoint);
if (!schema) {
console.warn(`⚠️ No schema registered for ${endpoint}`);
return rawResponse;
}
try {
const validated = await this.validateAndNormalize(rawResponse, schema);
return validated;
} catch (error) {
console.error(`❌ Response validation failed for ${endpoint}:`, error);
console.log('Raw response:', rawResponse);
throw new Error(`Invalid API response structure: ${error.message}`);
}
}
async validateAndNormalize(data, schema) {
const normalized = {};
for (const [key, config] of Object.entries(schema)) {
const value = data[key];
// Campo requerido faltante
if (config.required && (value === undefined || value === null)) {
throw new Error(`Required field '${key}' is missing`);
}
// Aplicar valor por defecto
if (value === undefined && config.default !== undefined) {
normalized[key] = config.default;
continue;
}
// Transformación de tipo
if (value !== undefined) {
normalized[key] = this.transformValue(value, config);
}
}
return normalized;
}
transformValue(value, config) {
switch (config.type) {
case 'array':
return Array.isArray(value) ? value : [value];
case 'string':
return String(value);
case 'number':
return Number(value);
case 'boolean':
return Boolean(value);
case 'date':
return new Date(value);
default:
return value;
}
}
}
// Configuración de schemas
const normalizer = new ApiResponseNormalizer();
normalizer.registerSchema('/api/users', {
users: { type: 'array', required: true, default: [] },
total: { type: 'number', required: false, default: 0 },
page: { type: 'number', required: false, default: 1 }
});
normalizer.registerSchema('/api/user/:id', {
id: { type: 'string', required: true },
name: { type: 'string', required: true },
email: { type: 'string', required: true },
created_at: { type: 'date', required: false }
});
// Uso en API client
class RobustApiClient {
constructor(normalizer) {
this.normalizer = normalizer;
}
async get(endpoint) {
const response = await fetch(endpoint);
const rawData = await response.json();
return this.normalizer.normalizeResponse(endpoint, rawData);
}
}
Problema #5: Error Handling Inconsistente Across APIs
Síntomas:
-
Algunos APIs usan HTTP status codes, otros no
-
Formatos de error completamente diferentes
-
Error messages que no ayudan al debugging
Unified Error Handler:
class UnifiedErrorHandler {
constructor() {
this.errorMappings = new Map();
this.setupDefaultMappings();
}
setupDefaultMappings() {
// Mapear errores comunes
this.errorMappings.set('NETWORK_ERROR', {
userMessage: 'Problema de conexión. Verifica tu internet.',
logLevel: 'warn',
retry: true
});
this.errorMappings.set('RATE_LIMITED', {
userMessage: 'Demasiadas solicitudes. Intenta en unos minutos.',
logLevel: 'info',
retry: true,
retryAfter: 60000
});
this.errorMappings.set('UNAUTHORIZED', {
userMessage: 'Sesión expirada. Por favor, inicia sesión.',
logLevel: 'warn',
retry: false,
action: 'redirect_login'
});
}
async handleApiError(error, context = {}) {
const errorInfo = this.categorizeError(error);
// Log structurado para debugging
console.group(`🚨 API Error in ${context.endpoint || 'unknown'}`);
console.log('Error Type:', errorInfo.type);
console.log('HTTP Status:', errorInfo.status);
console.log('Message:', errorInfo.message);
console.log('Context:', context);
console.log('Stack:', error.stack);
console.groupEnd();
// Reportar a servicio de logging si es necesario
if (errorInfo.logLevel === 'error') {
await this.reportError(errorInfo, context);
}
// Ejecutar acción según el tipo de error
await this.executeErrorAction(errorInfo, context);
return errorInfo;
}
categorizeError(error) {
// Network errors
if (error.message.includes('fetch') || error.message.includes('network')) {
return {
type: 'NETWORK_ERROR',
status: 0,
message: error.message,
...this.errorMappings.get('NETWORK_ERROR')
};
}
// HTTP errors
if (error.status) {
switch (error.status) {
case 401:
return {
type: 'UNAUTHORIZED',
status: 401,
message: 'Authentication required',
...this.errorMappings.get('UNAUTHORIZED')
};
case 429:
return {
type: 'RATE_LIMITED',
status: 429,
message: 'Rate limit exceeded',
...this.errorMappings.get('RATE_LIMITED')
};
case 500:
return {
type: 'SERVER_ERROR',
status: 500,
message: 'Internal server error',
userMessage: 'Error del servidor. Intenta más tarde.',
logLevel: 'error',
retry: true
};
default:
return {
type: 'HTTP_ERROR',
status: error.status,
message: error.message,
userMessage: 'Error inesperado. Intenta de nuevo.',
logLevel: 'warn',
retry: false
};
}
}
// Generic error
return {
type: 'UNKNOWN_ERROR',
status: 0,
message: error.message,
userMessage: 'Error inesperado. Por favor, reporta este problema.',
logLevel: 'error',
retry: false
};
}
async executeErrorAction(errorInfo, context) {
switch (errorInfo.action) {
case 'redirect_login':
window.location.href = '/login';
break;
case 'show_notification':
// Mostrar toast/notification
this.showUserNotification(errorInfo.userMessage, 'error');
break;
default:
// Default: mostrar error al usuario
this.showUserNotification(errorInfo.userMessage, 'error');
}
}
async reportError(errorInfo, context) {
// Enviar a servicio de logging (Sentry, LogRocket, etc.)
try {
await fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: errorInfo.type,
message: errorInfo.message,
status: errorInfo.status,
context,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href
})
});
} catch (loggingError) {
console.error('Failed to report error:', loggingError);
}
}
showUserNotification(message, type) {
// Implementar según tu sistema de notifications
console.log(`${type.toUpperCase()}: ${message}`);
}
}
Debugging Tools Para APIs
Network Inspector Avanzado:
// Interceptor para logging detallado
const originalFetch = window.fetch;
window.fetch = async function(...args) {
const [url, options = {}] = args;
console.group(`🌐 API Request: ${options.method || 'GET'} ${url}`);
console.log('Headers:', options.headers);
console.log('Body:', options.body);
const startTime = performance.now();
try {
const response = await originalFetch.apply(this, args);
const endTime = performance.now();
const duration = endTime - startTime;
console.log(`✅ Response: ${response.status} (${duration.toFixed(2)}ms)`);
console.log('Response Headers:', Object.fromEntries(response.headers.entries()));
// Clone para no consumir el stream
const clonedResponse = response.clone();
const responseBody = await clonedResponse.text();
console.log('Response Body:', responseBody);
console.groupEnd();
return response;
} catch (error) {
const endTime = performance.now();
const duration = endTime - startTime;
console.error(`❌ Request failed (${duration.toFixed(2)}ms):`, error);
console.groupEnd();
throw error;
}
};
API Health Monitoring
class ApiHealthMonitor {
constructor() {
this.metrics = {
totalRequests: 0,
successfulRequests: 0,
failedRequests: 0,
averageResponseTime: 0,
responseTimeHistory: []
};
}
recordRequest(success, responseTime) {
this.metrics.totalRequests++;
if (success) {
this.metrics.successfulRequests++;
} else {
this.metrics.failedRequests++;
}
this.metrics.responseTimeHistory.push(responseTime);
// Mantener solo las últimas 100 mediciones
if (this.metrics.responseTimeHistory.length > 100) {
this.metrics.responseTimeHistory.shift();
}
this.metrics.averageResponseTime =
this.metrics.responseTimeHistory.reduce((a, b) => a + b, 0) /
this.metrics.responseTimeHistory.length;
}
getHealthStatus() {
const successRate = this.metrics.successfulRequests / this.metrics.totalRequests;
return {
status: successRate > 0.95 ? 'healthy' : successRate > 0.8 ? 'degraded' : 'unhealthy',
successRate: (successRate * 100).toFixed(2),
averageResponseTime: this.metrics.averageResponseTime.toFixed(2),
totalRequests: this.metrics.totalRequests
};
}
}
const apiMonitor = new ApiHealthMonitor();
Checklist de API Integration Debugging
Pre-Development:
-
¿API documentation actualizada y correcta?
-
¿Rate limits y timeouts documentados?
-
¿Formato de respuestas consistente?
-
¿Error handling bien definido?
Development:
-
¿CORS configurado correctamente?
-
¿Authentication token management implementado?
-
¿Response validation en lugar?
-
¿Retry logic para requests fallidos?
Testing:
-
¿Tests para diferentes response scenarios?
-
¿Error cases cubiertas?
-
¿Performance bajo carga probada?
-
¿Timeout scenarios validados?
Production:
-
¿Monitoring de API health activo?
-
¿Error reporting configurado?
-
¿Alertas para rate limiting?
-
¿Logs estructurados para debugging?
Datos de Interés
Según estudios de GitHub, 67% de los bugs en aplicaciones web están relacionados con integraciones de APIs mal manejadas. Los problemas más comunes:
-
32% - Authentication y autorización
-
28% - Error handling inconsistente
-
23% - Rate limiting no manejado
-
17% - Response format assumptions
Conversación Abierta
¿Cuál de estos problemas de API les ha dado más quebraderos de cabeza? ¿Tienen alguna técnica de debugging que no mencioné?
Las integraciones robustas no se construyen evitando errores, sino manejándolos elegantemente. La diferencia entre una app que funciona “a veces” y una que funciona “siempre” está en los detalles del error handling.
Compartamos experiencias para crear integraciones más resilientes y debuggeables.
#TroubleshootingTuesday apiintegration debugging #ErrorHandling webdev javascript cors authentication