🔧 Troubleshooting Tuesday: API Integration Debugging - Los Problemas Que Destruyen Integraciones

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
  • Preflight OPTIONS requests que fallan

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

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

:stopwatch: Problema #2: Timeout e Rate Limiting - Requests Que Fallan Misteriosamente

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

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

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

:locked_with_key: Problema #3: Authentication Inconsistente - Tokens Que Funcionan “A Veces”

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

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

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

:bar_chart: Problema #4: Response Handling Inconsistente - Data Que “Desaparece”

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

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

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

:globe_with_meridians: Problema #5: Network Connectivity Issues - Requests Que Fallan Por Red

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

:magnifying_glass_tilted_left: 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 = [];
    }
}

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

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

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

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

#TroubleshootingTuesday apiintegration cors webdev debugging networktesting authentication