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
- Preflight OPTIONS requests que fallan
Código Problemático:
// ❌ Frontend sin entender CORS
fetch('https://api.external-service.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'secret-key', // Trigger preflight
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({ data: 'value' })
});
// Error: Access to fetch at 'https://api.external-service.com/data'
// from origin 'https://myapp.com' has been blocked by CORS policy
Debugging CORS Sistemático:
// Herramienta de debugging CORS
class CORSDebugger {
static async testEndpoint(url, options = {}) {
console.group(`🌐 Testing CORS for: ${url}`);
// 1. Test simple request primero
try {
const simpleResponse = await fetch(url, {
method: 'GET',
// Sin headers custom
});
console.log('✅ Simple request works:', simpleResponse.status);
} catch (error) {
console.error('❌ Simple request failed:', error.message);
}
// 2. Test preflight con OPTIONS
try {
const preflightResponse = await fetch(url, {
method: 'OPTIONS',
headers: {
'Origin': window.location.origin,
'Access-Control-Request-Method': options.method || 'POST',
'Access-Control-Request-Headers': Object.keys(options.headers || {}).join(',')
}
});
console.log('Preflight status:', preflightResponse.status);
console.log('CORS Headers:', {
'Access-Control-Allow-Origin': preflightResponse.headers.get('Access-Control-Allow-Origin'),
'Access-Control-Allow-Methods': preflightResponse.headers.get('Access-Control-Allow-Methods'),
'Access-Control-Allow-Headers': preflightResponse.headers.get('Access-Control-Allow-Headers'),
'Access-Control-Max-Age': preflightResponse.headers.get('Access-Control-Max-Age')
});
} catch (error) {
console.error('❌ Preflight failed:', error.message);
}
// 3. Test actual request
try {
const actualResponse = await fetch(url, options);
console.log('✅ Actual request works:', actualResponse.status);
} catch (error) {
console.error('❌ Actual request failed:', error.message);
}
console.groupEnd();
}
}
// Uso para debugging
CORSDebugger.testEndpoint('https://api.external-service.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'secret-key'
}
});
Solución Backend (Express.js):
// ✅ Configuración CORS correcta
const cors = require('cors');
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = [
'https://myapp.com',
'https://www.myapp.com',
'http://localhost:3000' // Development
];
// Allow requests with no origin (mobile apps, postman, etc)
if (!origin) return callback(null, true);
if (allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true, // Allow cookies
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: [
'Content-Type',
'Authorization',
'X-API-Key',
'X-Requested-With'
],
maxAge: 86400 // Cache preflight for 24 hours
};
app.use(cors(corsOptions));
// Logging para debugging
app.use((req, res, next) => {
console.log('🌍 CORS Request:', {
origin: req.headers.origin,
method: req.method,
headers: req.headers
});
next();
});
Problema #2: Timeout e Rate Limiting - Requests Que Fallan Misteriosamente
Síntomas:
- Requests que fallan después de exactamente X segundos
- “Too Many Requests” errors esporádicos
- Performance degradada con carga
- Requests que funcionan individualmente pero fallan en lotes
Identificación de Timeouts:
// ❌ Sin manejo de timeouts ni retry
async function fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
return response.json(); // Puede fallar sin explicación
}
Implementación Robusta:
// Configuración de timeouts y retry
class APIClient {
constructor(baseURL, options = {}) {
this.baseURL = baseURL;
this.timeout = options.timeout || 10000;
this.retryAttempts = options.retryAttempts || 3;
this.retryDelay = options.retryDelay || 1000;
this.rateLimitQueue = [];
this.requestsInLastMinute = 0;
this.maxRequestsPerMinute = options.rateLimit || 60;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const startTime = Date.now();
// Rate limiting check
await this.checkRateLimit();
// Configurar timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, this.timeout);
const requestOptions = {
...options,
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
...options.headers
}
};
let attempt = 1;
while (attempt <= this.retryAttempts) {
try {
console.log(`🚀 Attempt ${attempt}/${this.retryAttempts} for ${url}`);
const response = await fetch(url, requestOptions);
clearTimeout(timeoutId);
const duration = Date.now() - startTime;
console.log(`⏱️ Request completed in ${duration}ms`);
// Handle specific status codes
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
throw new RateLimitError(`Rate limited. Retry after ${retryAfter}s`, retryAfter);
}
if (!response.ok) {
throw new APIError(`HTTP ${response.status}: ${response.statusText}`, response.status);
}
return response;
} catch (error) {
console.error(`❌ Attempt ${attempt} failed:`, error.message);
if (attempt === this.retryAttempts) {
throw error;
}
// Exponential backoff para retries
const delay = this.retryDelay * Math.pow(2, attempt - 1);
// Rate limiting handling
if (error instanceof RateLimitError) {
const waitTime = parseInt(error.retryAfter) * 1000 || delay;
console.log(`⏳ Rate limited, waiting ${waitTime}ms`);
await this.sleep(waitTime);
} else if (error.name === 'AbortError') {
console.log(`⏳ Timeout, waiting ${delay}ms before retry`);
await this.sleep(delay);
} else if (this.isRetryableError(error)) {
await this.sleep(delay);
} else {
// Non-retryable error
throw error;
}
attempt++;
}
}
}
async checkRateLimit() {
// Simple rate limiting implementation
const now = Date.now();
const oneMinuteAgo = now - 60000;
// Clean old requests
this.rateLimitQueue = this.rateLimitQueue.filter(time => time > oneMinuteAgo);
if (this.rateLimitQueue.length >= this.maxRequestsPerMinute) {
const oldestRequest = Math.min(...this.rateLimitQueue);
const waitTime = 60000 - (now - oldestRequest);
console.log(`🚦 Rate limit reached, waiting ${waitTime}ms`);
await this.sleep(waitTime);
return this.checkRateLimit();
}
this.rateLimitQueue.push(now);
}
isRetryableError(error) {
// Network errors
if (error.name === 'TypeError' && error.message.includes('fetch')) {
return true;
}
// Timeout errors
if (error.name === 'AbortError') {
return true;
}
// Server errors (5xx)
if (error instanceof APIError && error.status >= 500) {
return true;
}
return false;
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Custom error classes
class APIError extends Error {
constructor(message, status) {
super(message);
this.name = 'APIError';
this.status = status;
}
}
class RateLimitError extends Error {
constructor(message, retryAfter) {
super(message);
this.name = 'RateLimitError';
this.retryAfter = retryAfter;
}
}
// Uso
const apiClient = new APIClient('https://api.example.com', {
timeout: 15000,
retryAttempts: 3,
rateLimit: 100
});
try {
const response = await apiClient.request('/users/123');
const userData = await response.json();
} catch (error) {
console.error('API request failed:', error);
}
Problema #3: Authentication Inconsistente - Tokens Que Funcionan “A Veces”
Síntomas:
- 401 errors esporádicos con tokens válidos
- Authentication que funciona en desarrollo pero falla en producción
- Headers de autorización que se pierden mysteriosamente
- Refresh token loops infinitos
Debugging de Auth Headers:
// Interceptor para debugging auth
class AuthInterceptor {
constructor(tokenManager) {
this.tokenManager = tokenManager;
}
async intercept(request) {
console.group('🔐 Auth Debug');
// Log del token actual
const token = await this.tokenManager.getToken();
console.log('Current token:', token ? `${token.substring(0, 20)}...` : 'null');
console.log('Token expires at:', this.tokenManager.getExpiryTime());
console.log('Time until expiry:', this.tokenManager.getTimeUntilExpiry());
// Verificar headers
console.log('Request headers:', request.headers);
// Perform request
const response = await fetch(request.url, request.options);
console.log('Response status:', response.status);
console.log('Response headers:', Object.fromEntries(response.headers.entries()));
// Handle auth failures
if (response.status === 401) {
console.error('❌ Authentication failed');
console.log('Attempting token refresh...');
try {
await this.tokenManager.refreshToken();
console.log('✅ Token refreshed successfully');
// Retry original request
const retryResponse = await fetch(request.url, {
...request.options,
headers: {
...request.options.headers,
'Authorization': `Bearer ${await this.tokenManager.getToken()}`
}
});
console.groupEnd();
return retryResponse;
} catch (refreshError) {
console.error('❌ Token refresh failed:', refreshError);
console.groupEnd();
throw new Error('Authentication failed and refresh unsuccessful');
}
}
console.groupEnd();
return response;
}
}
Token Manager Robusto:
class TokenManager {
constructor() {
this.accessToken = null;
this.refreshToken = null;
this.expiryTime = null;
this.refreshPromise = null;
}
async getToken() {
// Check if token needs refresh
if (!this.accessToken || this.needsRefresh()) {
return await this.refreshToken();
}
return this.accessToken;
}
needsRefresh() {
if (!this.expiryTime) return true;
// Refresh 5 minutes before expiry
const bufferTime = 5 * 60 * 1000;
return Date.now() > (this.expiryTime - bufferTime);
}
async refreshToken() {
// Prevent multiple simultaneous refresh attempts
if (this.refreshPromise) {
return await this.refreshPromise;
}
this.refreshPromise = this.performRefresh();
try {
const result = await this.refreshPromise;
return result;
} finally {
this.refreshPromise = null;
}
}
async performRefresh() {
if (!this.refreshToken) {
throw new Error('No refresh token available');
}
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
refresh_token: this.refreshToken
})
});
if (!response.ok) {
this.clearTokens();
throw new Error('Token refresh failed');
}
const tokenData = await response.json();
this.setTokens(tokenData);
return this.accessToken;
}
setTokens({ access_token, refresh_token, expires_in }) {
this.accessToken = access_token;
this.refreshToken = refresh_token || this.refreshToken;
this.expiryTime = Date.now() + (expires_in * 1000);
// Save to storage
this.saveToStorage();
}
clearTokens() {
this.accessToken = null;
this.refreshToken = null;
this.expiryTime = null;
localStorage.removeItem('auth_tokens');
}
saveToStorage() {
localStorage.setItem('auth_tokens', JSON.stringify({
access_token: this.accessToken,
refresh_token: this.refreshToken,
expires_in: this.expiryTime
}));
}
getExpiryTime() {
return this.expiryTime ? new Date(this.expiryTime) : null;
}
getTimeUntilExpiry() {
if (!this.expiryTime) return null;
const remaining = this.expiryTime - Date.now();
return remaining > 0 ? remaining : 0;
}
}
Problema #4: Response Handling Inconsistente - Data Que “Desaparece”
Síntomas:
- Responses que a veces están vacías
- JSON parsing errors esporádicos
- Data que existe en Network tab pero no llega al código
- Inconsistencias entre development y producción
Response Debugging:
// ❌ Handling básico que no captura edge cases
async function fetchData() {
const response = await fetch('/api/data');
return response.json(); // Puede fallar silenciosamente
}
Response Handler Completo:
class ResponseHandler {
static async process(response, options = {}) {
const {
expectedStatus = [200],
validateJSON = true,
logResponse = false
} = options;
console.group('📨 Processing Response');
console.log('Status:', response.status);
console.log('Headers:', Object.fromEntries(response.headers.entries()));
// Clone response for potential debugging
const responseClone = response.clone();
try {
// Status validation
if (!expectedStatus.includes(response.status)) {
throw new APIError(`Unexpected status: ${response.status}`, response.status);
}
// Content-Type validation
const contentType = response.headers.get('content-type');
console.log('Content-Type:', contentType);
if (!contentType) {
console.warn('⚠️ No Content-Type header');
}
// Handle different response types
let data;
if (contentType?.includes('application/json')) {
const textResponse = await response.text();
if (logResponse) {
console.log('Raw response:', textResponse);
}
if (!textResponse.trim()) {
console.warn('⚠️ Empty response body');
data = null;
} else {
try {
data = JSON.parse(textResponse);
} catch (parseError) {
console.error('❌ JSON Parse Error:', parseError);
console.log('Raw text:', textResponse.substring(0, 200));
throw new Error(`Invalid JSON response: ${parseError.message}`);
}
}
} else if (contentType?.includes('text/')) {
data = await response.text();
} else if (contentType?.includes('application/octet-stream')) {
data = await response.arrayBuffer();
} else {
data = await response.blob();
}
// Response validation
if (validateJSON && typeof data === 'object' && data !== null) {
this.validateResponseStructure(data);
}
console.log('✅ Response processed successfully');
console.groupEnd();
return data;
} catch (error) {
console.error('❌ Response processing failed:', error);
// Additional debugging info
try {
const debugText = await responseClone.text();
console.log('Debug - Raw response:', debugText.substring(0, 500));
} catch (debugError) {
console.log('Could not read response for debugging');
}
console.groupEnd();
throw error;
}
}
static validateResponseStructure(data) {
// Common API response patterns
if (Array.isArray(data)) {
console.log(`📋 Array response with ${data.length} items`);
return;
}
if (typeof data === 'object') {
const keys = Object.keys(data);
console.log(`📦 Object response with keys: ${keys.join(', ')}`);
// Check for common error indicators
if (data.error || data.errors) {
console.warn('⚠️ Response contains error field:', data.error || data.errors);
}
// Check for pagination metadata
if (data.pagination || data.meta) {
console.log('📄 Paginated response detected');
}
}
}
}
// Uso en requests
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`, {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
const userData = await ResponseHandler.process(response, {
expectedStatus: [200],
validateJSON: true,
logResponse: process.env.NODE_ENV === 'development'
});
return userData;
} catch (error) {
console.error(`Failed to fetch user ${userId}:`, error);
throw error;
}
}
Problema #5: Network Connectivity Issues - Requests Que Fallan Por Red
Síntomas:
- Requests que funcionan con WiFi pero fallan con datos móviles
- Timeouts en redes lentas
- Inconsistencias en diferentes ubicaciones geográficas
- Fallos en redes corporativas con proxy
Network Diagnostics:
class NetworkDiagnostics {
static async diagnose() {
console.group('🌐 Network Diagnostics');
// Connection info
if ('connection' in navigator) {
const connection = navigator.connection;
console.log('Connection type:', connection.effectiveType);
console.log('Downlink speed:', connection.downlink, 'Mbps');
console.log('RTT:', connection.rtt, 'ms');
console.log('Data saver:', connection.saveData);
}
// Online status
console.log('Online status:', navigator.onLine);
// Test connectivity to different endpoints
const endpoints = [
'https://httpbin.org/status/200',
'https://jsonplaceholder.typicode.com/posts/1',
'https://api.github.com'
];
for (const endpoint of endpoints) {
await this.testEndpoint(endpoint);
}
console.groupEnd();
}
static async testEndpoint(url) {
const startTime = Date.now();
try {
const response = await fetch(url, {
method: 'HEAD',
cache: 'no-cache'
});
const duration = Date.now() - startTime;
console.log(`✅ ${url}: ${response.status} (${duration}ms)`);
} catch (error) {
const duration = Date.now() - startTime;
console.error(`❌ ${url}: ${error.message} (${duration}ms)`);
}
}
static startConnectionMonitoring() {
// Monitor online/offline events
window.addEventListener('online', () => {
console.log('🟢 Connection restored');
this.onConnectionRestored();
});
window.addEventListener('offline', () => {
console.log('🔴 Connection lost');
this.onConnectionLost();
});
// Monitor connection changes
if ('connection' in navigator) {
navigator.connection.addEventListener('change', () => {
console.log('🔄 Connection changed:', {
type: navigator.connection.effectiveType,
downlink: navigator.connection.downlink,
rtt: navigator.connection.rtt
});
});
}
}
static onConnectionRestored() {
// Retry failed requests
// Update UI status
// Resume data sync
}
static onConnectionLost() {
// Queue pending requests
// Show offline indicator
// Enable offline mode
}
}
// Network-aware request handler
class NetworkAwareClient {
constructor() {
this.requestQueue = [];
this.isOnline = navigator.onLine;
NetworkDiagnostics.startConnectionMonitoring();
}
async request(url, options = {}) {
// Check connection quality
if ('connection' in navigator) {
const connection = navigator.connection;
// Adjust timeout based on connection
if (connection.effectiveType === 'slow-2g') {
options.timeout = 30000;
} else if (connection.effectiveType === '2g') {
options.timeout = 20000;
}
// Skip non-critical requests on slow connections
if (connection.saveData && options.priority === 'low') {
console.log('⏭️ Skipping low priority request due to data saver');
return null;
}
}
// Queue requests when offline
if (!this.isOnline) {
console.log('📋 Queueing request for when online:', url);
this.requestQueue.push({ url, options });
return null;
}
return fetch(url, options);
}
async processQueue() {
console.log(`🔄 Processing ${this.requestQueue.length} queued requests`);
const results = await Promise.allSettled(
this.requestQueue.map(({ url, options }) => fetch(url, options))
);
results.forEach((result, index) => {
if (result.status === 'rejected') {
console.error(`❌ Queued request ${index} failed:`, result.reason);
}
});
this.requestQueue = [];
}
}
Herramientas de Debugging API Integration
Network Tab Analysis:
// Script para analizar requests en DevTools
function analyzeNetworkRequests() {
const entries = performance.getEntriesByType('navigation')
.concat(performance.getEntriesByType('resource'));
const apiRequests = entries.filter(entry =>
entry.name.includes('/api/') ||
entry.name.includes('api.')
);
console.table(apiRequests.map(entry => ({
url: entry.name,
duration: Math.round(entry.duration),
size: entry.transferSize,
status: entry.responseStatus || 'unknown'
})));
}
API Testing Suite:
class APITestSuite {
constructor(baseURL) {
this.baseURL = baseURL;
this.results = [];
}
async runTests() {
const tests = [
() => this.testCORS(),
() => this.testAuthentication(),
() => this.testRateLimit(),
() => this.testTimeout(),
() => this.testErrorHandling()
];
for (const test of tests) {
try {
await test();
} catch (error) {
console.error('Test failed:', error);
}
}
this.generateReport();
}
async testCORS() {
console.log('🧪 Testing CORS...');
// Test implementation
}
async testAuthentication() {
console.log('🧪 Testing Authentication...');
// Test implementation
}
generateReport() {
console.table(this.results);
}
}
API Integration Debugging Checklist
Pre-Integration:
- API documentation revisada completamente
- Endpoints testeados en Postman/Insomnia
- Rate limits y quotas identificados
- Authentication flow documentado
Durante Development:
- CORS configurado correctamente en ambos lados
- Error handling implementado para todos los status codes
- Retry logic con exponential backoff
- Request/response logging en desarrollo
Pre-Production:
- Load testing realizado
- Network resilience testeada
- Error monitoring configurado
- Fallback strategies implementadas
Pro Tips de API Integration
1. Always Log Request/Response Context:
const logRequest = (url, options) => {
console.log(`→ ${options.method || 'GET'} ${url}`, {
headers: options.headers,
timestamp: new Date().toISOString()
});
};
2. Implement Circuit Breaker Pattern:
class CircuitBreaker {
constructor(threshold = 5, resetTime = 60000) {
this.threshold = threshold;
this.resetTime = resetTime;
this.failureCount = 0;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.nextAttempt = Date.now();
}
async execute(fn) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
} else {
this.state = 'HALF_OPEN';
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failureCount++;
if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.resetTime;
}
}
}
Experiencias de la Comunidad
¿Cuál de estos problemas de API integration les ha dado más dolores de cabeza?
¿Qué herramientas usan para debuggear integraciones complejas?
¿Han tenido que lidiar con APIs inconsistentes o mal documentadas? ¿Cómo lo resolvieron?
¿Qué estrategias usan para testing de integraciones en diferentes ambientes?
Las integraciones de APIs pueden ser frustrantes, pero con debugging sistemático y herramientas adecuadas, la mayoría de los problemas tienen soluciones claras. La clave está en logging detallado, manejo robusto de errores, y testing exhaustivo.
La próxima semana continuamos con “Work In Progress Wednesday” - ¡nos vemos el miércoles! ![]()
#TroubleshootingTuesday apiintegration cors webdev debugging networktesting authentication
