🔧 Troubleshooting Tuesday: API Integration Debugging - The 5 Problems That Destroy Integrations

¡Buenos días, dev community! :glowing_star:

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.

:prohibited: Problema #1: CORS Errors - El Terror del Frontend

:cross_mark: Síntomas:

  • Access-Control-Allow-Origin errors en consola

  • Requests que funcionan en Postman pero fallan en browser

  • Headers personalizados que desaparecen misteriosamente

:magnifying_glass_tilted_left: 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)
});

:white_check_mark: 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;
    }
}

:light_bulb: 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 });
    }
});

:alarm_clock: Problema #2: Timeouts y Rate Limiting Impredecibles

:cross_mark: Síntomas:

  • Requests que funcionan a veces y fallan otras

  • 429 (Too Many Requests) errors esporádicos

  • Aplicación que se cuelga esperando respuestas

:magnifying_glass_tilted_left: 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');

:locked_with_key: Problema #3: Authentication Token Hell

:cross_mark: Síntomas:

  • 401 Unauthorized errors intermitentes

  • Tokens que expiran en medio de operaciones

  • Refresh token loops infinitos

:white_check_mark: 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;
    }
}

:globe_with_meridians: Problema #4: Response Format Inconsistencies

:cross_mark: Síntomas:

  • APIs que a veces retornan arrays, a veces objetos

  • Campos que aparecen y desaparecen

  • Estructuras de error inconsistentes

:white_check_mark: 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);
    }
}

:bar_chart: Problema #5: Error Handling Inconsistente Across APIs

:cross_mark: Síntomas:

  • Algunos APIs usan HTTP status codes, otros no

  • Formatos de error completamente diferentes

  • Error messages que no ayudan al debugging

:white_check_mark: 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}`);
    }
}

:hammer_and_wrench: 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;
    }
};

:bar_chart: 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();

:light_bulb: Checklist de API Integration Debugging

:white_check_mark: Pre-Development:

  • ¿API documentation actualizada y correcta?

  • ¿Rate limits y timeouts documentados?

  • ¿Formato de respuestas consistente?

  • ¿Error handling bien definido?

:white_check_mark: Development:

  • ¿CORS configurado correctamente?

  • ¿Authentication token management implementado?

  • ¿Response validation en lugar?

  • ¿Retry logic para requests fallidos?

:white_check_mark: Testing:

  • ¿Tests para diferentes response scenarios?

  • ¿Error cases cubiertas?

  • ¿Performance bajo carga probada?

  • ¿Timeout scenarios validados?

:white_check_mark: Production:

  • ¿Monitoring de API health activo?

  • ¿Error reporting configurado?

  • ¿Alertas para rate limiting?

  • ¿Logs estructurados para debugging?

:bullseye: 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

:speech_balloon: 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