Skip to content

Troubleshooting

Runbook de problemas comunes que se encuentran en dev y prod, con el diagnóstico y fix probado.

Logs primero

El 80% de los bugs se resuelven leyendo logs. Antes de cambiar código, siempre empieza por:

bash
# Laravel app logs
tail -f storage/logs/laravel-$(date +%F).log

# Nginx access
tail -f /var/log/nginx/humae_api_access.log

# Nginx errors
tail -f /var/log/nginx/humae_api_error.log

# Queue worker
sudo supervisorctl tail -f humae-queue:humae-queue_00

# MySQL slow queries
tail -f /var/log/mysql/slow.log

Filtros útiles:

bash
# Ver solo errores del backend
tail -f storage/logs/laravel-*.log | grep -E "(ERROR|CRITICAL|ALERT)"

# Ver webhooks de Stripe
tail -f storage/logs/laravel-*.log | grep -i stripe

# Ver solo la app sin noise
tail -f storage/logs/laravel-*.log | grep -v "telescope\|scout\|debugbar"

Problemas comunes

1 · "SQLSTATE[HY000] [2002] Connection refused" en migraciones

Causa: MySQL no está corriendo o DB_HOST es incorrecto.

Fix:

bash
# Verificar que MySQL esté activo
sudo systemctl status mysql
# Si no: sudo systemctl start mysql

# Verificar conexión
mysql -u $DB_USERNAME -p$DB_PASSWORD -h $DB_HOST $DB_DATABASE -e "SELECT 1;"

# Si usas Docker:
docker compose ps
# Si mysql no está running: docker compose up -d mysql

2 · Error 419 CSRF token mismatch (frontend SPA)

Causa: la cookie XSRF-TOKEN no llega o el header X-XSRF-TOKEN no se envía.

Fix:

  1. El frontend debe llamar GET /sanctum/csrf-cookie antes del primer POST.
  2. Revisar SANCTUM_STATEFUL_DOMAINS — debe incluir el dominio del frontend.
  3. Revisar SESSION_DOMAIN — debe cubrir ambos subdominios (ej. .humae.com.mx).
  4. Axios en el frontend debe tener withCredentials: true.

3 · "Cannot redeclare function X" al correr tests

Causa: helpers de test definidos con mismo nombre en archivos distintos.

Fix: renombrar uno, o mover a una clase helper:

php
// Antes (colisiona):
function makeActiveCandidate(): CandidateProfile { /*...*/ }  // en dos archivos

// Después:
function directoryTestMakeActiveCandidate(): CandidateProfile { /*...*/ }
function pipelineTestMakeActiveCandidate(): CandidateProfile { /*...*/ }

Mejor opción: tests/Support/TestData.php con métodos estáticos.

4 · Migration falla con "Specified key was too long"

Causa: el nombre auto-generado del índice supera 64 chars (límite de MySQL).

Fix: asigna nombre explícito:

php
// Antes
$table->unique(['psychometric_attempt_id', 'psychometric_question_id']);

// Después
$table->unique(
    ['psychometric_attempt_id', 'psychometric_question_id'],
    'uq_pa_pq',
);

5 · Migration falla con "Invalid default value for 'birth_date'"

Causa: MySQL 8 con modo NO_ZERO_DATE rechaza timestamp default 0000-00-00.

Fix: usar datetime en vez de timestamp:

php
// Antes
$table->timestamp('birth_date')->nullable();

// Después
$table->datetime('birth_date')->nullable();

6 · PHPStan nivel 8 falla con "chain of method calls on string"

Causa: faltan @property docblocks en el modelo, PHPStan infiere string para columnas.

Fix: agregar docblock completo:

php
/**
 * @property int $id
 * @property string $title
 * @property VacancyState $state
 * @property \Carbon\Carbon|null $published_at
 * @property \Carbon\Carbon $created_at
 * @property-read Company $company
 */
class Vacancy extends Model { /*...*/ }

Alternativa: php artisan ide-helper:models -W auto-genera.

7 · PHPStan falla en relaciones HasMany<X, self>

Causa: Laravel 12 cambió covariance. El segundo type param debe ser $this no self.

Fix:

php
// Antes (Laravel 11 style)
/** @return HasMany<VacancyAssignment, self> */
public function assignments(): HasMany { /*...*/ }

// Después (Laravel 12 style)
/** @return HasMany<VacancyAssignment, $this> */
public function assignments(): HasMany { /*...*/ }

Para reemplazar en todos los archivos de una:

bash
find app/Models -name "*.php" -exec sed -i '' 's/, self>/, \$this>/g' {} \;

8 · Stripe webhook no llega al backend

Diagnóstico paso a paso:

bash
# 1. ¿El endpoint es accesible desde internet?
curl -i https://api.humae.com.mx/api/webhooks/stripe -X POST -d '{}'
# Debe devolver 400 (firma inválida), no 404 ni 502

# 2. ¿Stripe está disparando?
# Dashboard Stripe → Webhooks → tu endpoint → últimos eventos
# ¿Aparecen eventos con status "failed"? Ver error

# 3. ¿El signing secret es el correcto?
# En .env: echo $STRIPE_WEBHOOK_SECRET
# En Stripe dashboard: el "Signing secret" de tu endpoint
# Deben coincidir exactamente

# 4. ¿El backend recibe el POST?
tail -f storage/logs/laravel-*.log | grep -i webhook

Fix común: Cloudflare proxy rechazando el payload grande.

Solución: regla WAF bypass para POST /api/webhooks/*.

9 · "Field 'X' doesn't have a default value" al insertar

Causa: la columna es NOT NULL en DB pero el factory o create no la provee.

Fix: agregar al factory o pasar el valor:

php
// Factory
'level' => fake()->randomElement(['basico', 'intermedio', 'avanzado']),

// O en attach pivot:
$profile->skills()->attach([
    $skillId => ['level' => 'avanzado'],  // pivot columns
]);

10 · /storage/avatars/... devuelve 404

Causa: el symlink public/storage → storage/app/public no existe (no se corrió php artisan storage:link).

Fix:

bash
cd /var/www/humae_backend
php artisan storage:link
sudo chown -R www-data:www-data storage bootstrap/cache public/storage

Si el archivo físico está en storage/app/public/avatars/... pero igual devuelve 404, verificar que Nginx no esté rewriteando /storage/* al index.php y que el symlink sea legible por www-data.

11 · Emails no llegan al inbox

Diagnóstico:

bash
# 1. ¿Laravel intenta enviar?
tail -f storage/logs/laravel-*.log | grep -i mail

# 2. ¿Postfix recibe la conexión SMTP de Laravel?
sudo tail -f /var/log/mail.log

# 3. ¿Hay mensajes en la cola local?
mailq

# 4. ¿El MTA destino aceptó?
#   - status=sent  → OK
#   - status=deferred → reintentará
#   - status=bounced → revisar motivo (rechazo por reputación, dominio inexistente, etc.)

Fix comunes:

  • connect to 127.0.0.1:25: connection refused → Postfix no está corriendo. sudo systemctl start postfix.
  • Todo termina en spam → falta PTR, DKIM o SPF. Verificar con dig +short txt humae.com.mx y con el header Authentication-Results del correo recibido (debe decir dkim=pass spf=pass).
  • status=deferred (network is unreachable) → el provider cloud bloquea 25 saliente. Pedir desbloqueo o migrar de provider.
  • Queue no procesasupervisorctl status humae-queue:*.

12 · Queue worker no procesa jobs

Diagnóstico:

bash
# Ver estado
sudo supervisorctl status humae-queue:*
# Debe decir RUNNING

# Ver logs del worker
sudo supervisorctl tail -f humae-queue:humae-queue_00

# Ver jobs encolados
redis-cli -a $REDIS_PASSWORD LLEN queues:default
# > 100 puede indicar que el worker no está procesando

# Ver failed jobs
php artisan queue:failed

Fix comunes:

  • Worker crasheado → supervisorctl restart humae-queue:*.
  • Job demasiado grande (memory leak) → ajustar memory_limit en PHP.
  • Job > $timeout → aumentar el timeout del job o del worker.
  • Redis caído → systemctl status redis-server.

13 · "Target class [XYZ] does not exist" al resolver DI

Causa: namespace incorrecto, autoload no actualizado, o binding mal registrado.

Fix:

bash
composer dump-autoload

# Si es un binding custom, verificar AppServiceProvider::register()
php artisan config:clear
php artisan cache:clear

14 · OPcache muestra código viejo tras deploy

Causa: opcache.validate_timestamps=0 en prod (recomendado), pero no hiciste reload tras deploy.

Fix:

bash
sudo service php8.3-fpm reload

# O graceful:
sudo systemctl reload php8.3-fpm

Automatizar en el script de deploy.

15 · "Route [X] not defined" al llamar route('X')

Causa: php artisan route:cache cachea rutas viejas o la ruta no existe.

Fix:

bash
php artisan route:clear
php artisan route:list | grep <nombre>
# verificar que existe

# En prod: después de cambios en rutas
php artisan route:cache

16 · Tests pasan local pero fallan en CI

Causas comunes:

  1. Timezone diferente → fijar APP_TIMEZONE en tests.
  2. Dependencias de orden (tests que comparten estado) → revisar beforeEach y RefreshDatabase.
  3. Random data → usar seeds fijas con fake()->seed(1) o fixtures.
  4. DB driver diferente → MySQL vs SQLite tienen comportamiento sutil distinto (JSON columns, date parsing).

Debug local de CI:

bash
# Replicar CI env
APP_ENV=testing php artisan test

17 · Frontend CORS error

Síntoma: Access to XMLHttpRequest at 'https://api.humae.com.mx/...' from origin 'https://humae.com.mx' has been blocked by CORS policy

Fix:

  1. Verificar FRONTEND_URL en .env del backend.

  2. Verificar config/cors.php include el origen.

  3. Limpiar cache: php artisan config:clear.

  4. Si tienes múltiples orígenes (staging), separar con coma:

    env
    FRONTEND_URL=https://humae.com.mx,https://staging.humae.com.mx

    Y en config/cors.php:

    php
    'allowed_origins' => array_map('trim', explode(',', env('FRONTEND_URL', ''))),

18 · "Class 'Redis' not found"

Causa: extensión PHP redis no instalada.

Fix:

bash
sudo apt install php8.3-redis
sudo systemctl restart php8.3-fpm

O usar el cliente PHP puro:

env
REDIS_CLIENT=predis

Y en composer:

bash
composer require predis/predis

Performance diagnóstico

Queries lentas

sql
-- Activar slow log (my.cnf)
slow_query_log = 1
long_query_time = 1

-- Ver slow queries
tail -f /var/log/mysql/slow.log

Correr EXPLAIN en queries lentas. Si falta índice, agregar migration.

N+1 detection

Usar spatie/laravel-ray o [barryvdh/laravel-debugbar] en dev.

En prod: revisar logs de slow queries con SQL repetido.

Memory leaks en queue workers

Workers PHP acumulan memoria con el tiempo. Solución: --max-time=3600 (reciclar worker cada hora).

Debugging con Tinker

bash
php artisan tinker

Útil para:

php
// Ver estado actual
>>> \App\Models\User::count()
>>> \App\Models\Vacancy::where('state', 'activa')->count()

// Inspeccionar un modelo
>>> $u = \App\Models\User::find(1);
>>> $u->roles
>>> $u->candidateProfile

// Probar un service
>>> $service = app(\App\Services\MembershipService::class);
>>> $service->expireStale();

// Disparar una notification manualmente
>>> $u->notify(new \App\Notifications\WelcomeNotification());

// Ejecutar código arbitrario
>>> \DB::select('SELECT COUNT(*) as n FROM users');

Telescope en dev

Si el proyecto tiene Laravel Telescope instalado:

bash
php artisan telescope:install
php artisan migrate

Acceder en /telescope (solo dev). Muestra:

  • Todas las requests con payload, response, duration
  • Queries ejecutadas (detecta N+1)
  • Jobs dispatched + results
  • Notifications enviadas
  • Cache hits/misses
  • Exceptions

Nunca habilitar en prod — expone datos sensibles.

Cuándo hacer rollback

Solo si:

  • Un endpoint crítico está caído (auth, checkout, webhooks).
  • Corrupción de datos en curso.
  • Falla de seguridad expuesta.

Cómo:

  1. Identificar el commit bueno: git log.

  2. Deploy del commit anterior:

    bash
    # Si usas GitOps (Vercel, Render auto-deploy)
    git revert <bad-commit>
    git push
    
    # Si es manual
    ssh prod
    cd /var/www/humae_backend
    git checkout <good-sha>
    composer install --no-dev
    php artisan migrate:rollback  # SOLO si la migration causa el problema
    sudo service php8.3-fpm reload
  3. Restaurar DB desde backup si hubo corrupción.

No hacer rollback de migraciones sin pensar — pueden perder datos. Es mejor hacer una migration forward que revierta el comportamiento.

Contacto

  • Email: dev@humae.com.mx
  • Slack interno: #humae-backend
  • Issues en GitHub: github.com/tu-org/humae-backend/issues

Manual terminado. Siguiente recurso: API docs con Scribe o el checklist de deploy.

Manual de usuario HUMAE · Uso interno