44 KiB

Common Errors and Solutions

This guide covers the most frequently encountered errors in Changemaker Lite V2 and their solutions.

Overview

How to Use This Guide

  1. Find your error - Use the error code or message to locate the section
  2. Diagnose - Read the symptoms and causes
  3. Apply solution - Follow step-by-step instructions
  4. Prevent recurrence - Implement preventive measures

Error Severity Levels

Level Icon Meaning Action
Critical 🔴 System down or data at risk Fix immediately
High 🟠 Feature unavailable Fix within hours
Medium 🟡 Degraded performance Fix within days
Low 🟢 Minor inconvenience Fix when convenient

Quick Error Lookup

Error Code Category Page
401 Authentication Link
403 Authorization Link
404 Not Found Link
422 Validation Link
500 Server Error Link
CORS Frontend Link
ECONNREFUSED Database Link

Authentication Errors

401 Unauthorized

Severity: 🟠 High

Symptoms

{
  "error": "Unauthorized",
  "message": "Invalid or missing token"
}

Browser console:

Error: Request failed with status code 401

Common Causes

  1. Missing token - No Authorization header sent
  2. Expired token - Access token older than 15 minutes
  3. Invalid token - Corrupted or tampered token
  4. Wrong environment - Token from dev used in production

Solutions

Solution 1: Check if logged in

// In browser console
console.log(localStorage.getItem('auth-storage'));

If null or missing accessToken, you need to log in again.

Solution 2: Refresh token

The frontend automatically refreshes tokens. If this fails:

  1. Log out completely
  2. Clear localStorage: localStorage.clear()
  3. Log in again

Solution 3: Verify API configuration

Check admin/.env:

VITE_API_URL=http://localhost:4000  # Must match actual API URL

Solution 4: Check token expiration

// In browser console
const storage = JSON.parse(localStorage.getItem('auth-storage'));
const payload = JSON.parse(atob(storage.state.accessToken.split('.')[1]));
console.log('Token expires:', new Date(payload.exp * 1000));
console.log('Current time:', new Date());

If expired, the refresh interceptor should handle this. If not working:

# Check API logs
docker compose logs api | grep "refresh"

Prevention

  • Auto-refresh works - Frontend handles token refresh automatically
  • Long sessions - Refresh tokens valid for 7 days
  • Activity-based - Tokens refresh on API calls
  • Clear error handling - Frontend redirects to login on failure

!!! warning "Security Note" 401 errors may return generic messages to prevent user enumeration. This is intentional security behavior.


403 Forbidden

Severity: 🟠 High

Symptoms

{
  "error": "Forbidden",
  "message": "Insufficient permissions"
}

Or role-specific:

{
  "error": "Forbidden",
  "message": "Requires one of: SUPER_ADMIN, MAP_ADMIN"
}

Common Causes

  1. Wrong role - User lacks required role
  2. TEMP user - Temporary users restricted from most features
  3. Feature disabled - Feature flag not enabled
  4. Wrong endpoint - Using admin endpoint as public user

Solutions

Solution 1: Check user role

# In database
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
  -c "SELECT email, role FROM \"User\" WHERE email = 'your@email.com';"

Solution 2: Update user role

-- Via Prisma Studio (recommended)
docker compose exec api npx prisma studio
-- Navigate to User table, edit role

-- Or via SQL
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
  -c "UPDATE \"User\" SET role = 'MAP_ADMIN' WHERE email = 'your@email.com';"

Solution 3: Check feature flags

# In API logs
docker compose logs api | grep "ENABLE_"

# Check .env
cat .env | grep ENABLE

Solution 4: Verify endpoint permissions

Check api/src/modules/*/routes.ts:

// Admin endpoint
router.post('/', authenticate, requireRole('SUPER_ADMIN'), ...);

// Public endpoint (no auth)
router.get('/public', ...);

Prevention

  • Role-based access control - Clear role hierarchy
  • Explicit permissions - Each endpoint lists required roles
  • Audit trail - Track permission changes
  • Documentation - Role matrix in Access Control

Invalid Token

Severity: 🟠 High

Symptoms

{
  "error": "Unauthorized",
  "message": "Invalid token"
}

Or in API logs:

Error: jwt malformed
Error: invalid signature
Error: jwt must be provided

Common Causes

  1. Corrupted token - LocalStorage corruption
  2. Wrong secret - JWT_ACCESS_SECRET changed
  3. Modified token - Attempted tampering
  4. Format error - Not a valid JWT structure

Solutions

Solution 1: Clear and re-login

// In browser console
localStorage.clear();
// Then log in again

Solution 2: Verify JWT structure

Valid JWT has 3 parts separated by dots:

const token = 'header.payload.signature';
console.log(token.split('.').length); // Should be 3

Solution 3: Check secret configuration

# In .env
JWT_ACCESS_SECRET=your-secret-here-32-chars-min
JWT_REFRESH_SECRET=different-secret-here-32-chars-min

# Secrets must:
# - Be different from each other
# - Be at least 32 characters
# - Remain unchanged (changing invalidates all tokens)

Solution 4: Verify token in logs

# API logs show token validation errors
docker compose logs api | tail -100 | grep "jwt"

Prevention

  • Secure secrets - Use openssl rand -hex 32
  • Never commit secrets - Keep in .env (gitignored)
  • Rotate carefully - Changing secrets logs out all users
  • Monitor errors - Alert on spike in invalid token errors

Token Expired

Severity: 🟡 Medium

Symptoms

{
  "error": "Unauthorized",
  "message": "Token expired"
}

Or:

Error: jwt expired

Common Causes

  1. Access token expired - Normal after 15 minutes of inactivity
  2. Refresh token expired - Refresh token older than 7 days
  3. System clock skew - Server/client time mismatch
  4. Refresh failed - Refresh token invalid or revoked

Solutions

Solution 1: Automatic refresh

Frontend automatically refreshes tokens on 401. If this fails:

// Check refresh token in localStorage
const storage = JSON.parse(localStorage.getItem('auth-storage'));
console.log('Has refresh token:', !!storage?.state?.refreshToken);

Solution 2: Manual login

If refresh token expired (after 7 days):

  1. You'll be redirected to login automatically
  2. Log in with email/password
  3. New tokens issued

Solution 3: Check system time

# On server
date

# Sync if incorrect
sudo ntpdate -s time.nist.gov

Solution 4: Verify token expiration

# In API logs
docker compose logs api | grep "expired"

# Check token age
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
  -c "SELECT email, \"createdAt\", \"expiresAt\" FROM \"RefreshToken\" ORDER BY \"createdAt\" DESC LIMIT 10;"

Prevention

  • Sliding sessions - Tokens auto-refresh on activity
  • Long refresh window - 7-day refresh token validity
  • Graceful handling - Automatic re-login redirect
  • Activity tracking - Monitor token refresh patterns

!!! tip "Developer Tip" During development, use longer token expiration in .env: bash JWT_ACCESS_EXPIRATION=1d # Instead of 15m JWT_REFRESH_EXPIRATION=30d # Instead of 7d


User Not Found

Severity: 🟡 Medium

Symptoms

{
  "error": "Unauthorized",
  "message": "Invalid credentials"
}

Note: Same message for both "user not found" and "wrong password" (security feature).

Common Causes

  1. Wrong email - Typo in email address
  2. User deleted - Account removed from database
  3. Wrong database - Connected to wrong environment
  4. Case sensitivity - Email stored differently

Solutions

Solution 1: Verify user exists

# Check database
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
  -c "SELECT email, role FROM \"User\" WHERE email ILIKE '%search%';"

Solution 2: Check email format

Emails are stored lowercase:

-- Find user case-insensitive
SELECT * FROM "User" WHERE LOWER(email) = LOWER('User@Example.com');

Solution 3: Create user if missing

# Via API
curl -X POST http://localhost:4000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "password": "SecurePass123!",
    "name": "User Name"
  }'

# Or via admin UI at /app/users

Solution 4: Check database connection

# Verify correct database
docker compose exec api npx prisma db pull

# Check DATABASE_URL in .env
cat .env | grep DATABASE_URL

Prevention

  • Email validation - Enforce valid email format
  • Case normalization - Store emails lowercase
  • Soft deletes - Consider flagging instead of deleting
  • Audit trail - Log user deletions

API Errors

500 Internal Server Error

Severity: 🔴 Critical

Symptoms

{
  "error": "Internal Server Error",
  "message": "An unexpected error occurred"
}

Or frontend error:

Error: Request failed with status code 500

Common Causes

  1. Unhandled exception - Code threw unexpected error
  2. Database error - Query failed
  3. Missing environment variable - Required config missing
  4. Type error - Runtime type mismatch

Solutions

Solution 1: Check API logs

# View recent logs
docker compose logs api --tail=100

# Follow logs in real-time
docker compose logs -f api

# Search for errors
docker compose logs api | grep -i error | tail -20

Solution 2: Common error patterns

// Missing environment variable
Error: SMTP_HOST is required
// Solution: Add to .env

// Database connection error
Error: Can't reach database server at `v2-postgres:5432`
// Solution: Check database is running

// Type error
TypeError: Cannot read property 'id' of undefined
// Solution: Check code for null checks

Solution 3: Restart API

# Restart API container
docker compose restart api

# Or rebuild if code changed
docker compose up -d --build api

Solution 4: Enable debug logging

# In .env
LOG_LEVEL=debug

# Restart API
docker compose restart api

# Check detailed logs
docker compose logs api

Prevention

  • Error handling - Try/catch in all routes
  • Input validation - Validate all inputs with Zod
  • Type safety - Use TypeScript strictly
  • Health checks - Monitor API health
  • Alerting - Set up alerts for 500 errors

!!! warning "Production Alert" 500 errors indicate bugs. Always investigate and fix root cause.


400 Bad Request

Severity: 🟡 Medium

Symptoms

{
  "error": "Bad Request",
  "message": "Invalid request format"
}

Or with validation details:

{
  "error": "Bad Request",
  "message": "Validation failed: 2 errors"
}

Common Causes

  1. Invalid JSON - Malformed request body
  2. Wrong Content-Type - Missing or incorrect header
  3. Missing required field - Required parameter not sent
  4. Invalid data type - String sent for number field

Solutions

Solution 1: Check request format

// Correct format
const response = await api.post('/api/users', {
  email: 'user@example.com',
  password: 'SecurePass123!',
  name: 'User Name'
}, {
  headers: {
    'Content-Type': 'application/json'
  }
});

// Common mistakes:
// ❌ Missing Content-Type header
// ❌ Sending FormData to JSON endpoint
// ❌ Malformed JSON (trailing comma, unquoted keys)

Solution 2: Validate against schema

Check API schema in api/src/modules/*/schemas.ts:

// Example: User creation schema
export const createUserSchema = z.object({
  email: z.string().email(),
  password: z.string().min(12),
  name: z.string().min(1),
  role: z.enum(['USER', 'MAP_ADMIN', 'INFLUENCE_ADMIN', 'SUPER_ADMIN']).optional()
});

Solution 3: Check API logs for details

# Logs show validation errors
docker compose logs api | grep "Validation failed"

# Example output:
# Validation failed: {
#   "email": "Invalid email format",
#   "password": "Must be at least 12 characters"
# }

Solution 4: Test with curl

# Test request
curl -X POST http://localhost:4000/api/users \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "email": "test@example.com",
    "password": "SecurePass123!",
    "name": "Test User"
  }'

Prevention

  • Client-side validation - Validate before sending
  • TypeScript types - Use generated types from API
  • Schema documentation - Document all endpoints
  • Error messages - Clear validation error messages

404 Not Found

Severity: 🟢 Low to 🟡 Medium

Symptoms

{
  "error": "Not Found",
  "message": "Resource not found"
}

Or specific:

{
  "error": "Not Found",
  "message": "Campaign not found"
}

Common Causes

  1. Wrong ID - Resource doesn't exist
  2. Wrong URL - Typo in endpoint path
  3. Deleted resource - Resource was deleted
  4. Wrong HTTP method - GET instead of POST

Solutions

Solution 1: Verify resource exists

# Check database
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
  -c "SELECT id, name FROM \"Campaign\" WHERE id = 'YOUR_ID';"

Solution 2: Check URL format

// Correct formats
GET /api/campaigns/:id          // Single campaign
GET /api/campaigns              // List campaigns
POST /api/campaigns             // Create campaign
PUT /api/campaigns/:id          // Update campaign
DELETE /api/campaigns/:id       // Delete campaign

// Common mistakes:
// ❌ /api/campaign/:id (singular, should be plural)
// ❌ /api/campaigns/id/:id (extra 'id/' in path)
// ❌ /api/campaign (wrong singular/plural)

Solution 3: Check route registration

# API logs show registered routes on startup
docker compose logs api | grep "Registered route"

# Or check routes file
cat api/src/modules/*/routes.ts

Solution 4: Test endpoint

# List all campaigns to verify endpoint
curl http://localhost:4000/api/campaigns

# Test specific ID
curl http://localhost:4000/api/campaigns/YOUR_ID

Prevention

  • UUID validation - Validate ID format before querying
  • Soft deletes - Flag as deleted instead of removing
  • Resource existence checks - Verify before operations
  • Clear error messages - Specify which resource not found

422 Unprocessable Entity

Severity: 🟡 Medium

Symptoms

{
  "error": "Unprocessable Entity",
  "message": "Validation failed",
  "details": {
    "email": "Email already exists",
    "password": "Must contain uppercase, lowercase, and digit"
  }
}

Common Causes

  1. Business logic violation - Email already exists
  2. Data integrity - Foreign key doesn't exist
  3. Complex validation - Password requirements not met
  4. State conflict - Can't delete resource in use

Solutions

Solution 1: Read validation details

The details field shows exactly what's wrong:

try {
  await api.post('/api/users', userData);
} catch (error) {
  if (error.response?.status === 422) {
    console.log('Validation errors:', error.response.data.details);
    // Show to user field-by-field
  }
}

Solution 2: Check constraints

# Email uniqueness
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
  -c "SELECT email FROM \"User\" WHERE email = 'test@example.com';"

# Foreign key exists
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
  -c "SELECT id FROM \"Campaign\" WHERE id = 'CAMPAIGN_ID';"

Solution 3: Fix data

Common fixes:

// Email already exists → Use different email
email: 'newuser@example.com'

// Password too weak → Meet requirements
password: 'SecurePass123!'  // 12+ chars, upper, lower, digit

// Foreign key missing → Create parent first
// Create campaign before creating email

// Resource in use → Delete dependents first
// Delete locations before deleting cut

Solution 4: Check database schema

# View constraints
docker compose exec api npx prisma studio
# Navigate to model, see unique fields and relations

Prevention

  • Client validation - Check constraints before submitting
  • Clear requirements - Document validation rules
  • Helpful messages - Explain how to fix
  • Cascade deletes - Auto-delete dependents when safe

Database Errors

Connection Refused

Severity: 🔴 Critical

Symptoms

Error: connect ECONNREFUSED 127.0.0.1:5433

Or:

Error: Can't reach database server at `v2-postgres:5432`

Common Causes

  1. Database not running - Container stopped
  2. Wrong connection string - Incorrect host/port
  3. Network issue - Container can't reach database
  4. Port conflict - Port already in use

Solutions

Solution 1: Check database status

# List running containers
docker compose ps

# Database should show as "running"
# If not:
docker compose up -d v2-postgres

Solution 2: Verify connection string

# Check .env
cat .env | grep DATABASE_URL

# Should be (from API container):
DATABASE_URL="postgresql://changemaker:PASSWORD@v2-postgres:5432/changemaker_v2"

# Or (from host):
DATABASE_URL="postgresql://changemaker:PASSWORD@localhost:5433/changemaker_v2"

Solution 3: Check database logs

# View database logs
docker compose logs v2-postgres

# Look for:
# - "database system is ready to accept connections" (good)
# - "FATAL: password authentication failed" (bad - wrong password)
# - "port 5432 already in use" (bad - port conflict)

Solution 4: Test connection manually

# From host
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 -c "SELECT NOW();"

# Should return current timestamp
# If fails, database isn't running properly

Solution 5: Restart database

# Restart database container
docker compose restart v2-postgres

# Or recreate if corrupted
docker compose down v2-postgres
docker compose up -d v2-postgres

# Wait for "ready to accept connections" message
docker compose logs -f v2-postgres

Prevention

  • Health checks - Monitor database availability
  • Auto-restart - Configure restart policy
  • Connection pooling - Handle transient failures
  • Alerting - Alert on connection failures

Too Many Connections

Severity: 🟠 High

Symptoms

Error: too many connections for database "changemaker_v2"

Or:

Error: Prepared statement "prisma_xxx" already exists

Common Causes

  1. Connection leak - Connections not released
  2. Pool too small - Not enough connections for load
  3. Long-running queries - Blocking connections
  4. Multiple clients - Too many Prisma instances

Solutions

Solution 1: Check active connections

# View connections
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
  -c "SELECT count(*) FROM pg_stat_activity WHERE datname = 'changemaker_v2';"

# PostgreSQL default max: 100 connections
# Prisma default pool: 10 connections

Solution 2: Kill idle connections

-- Find idle connections
SELECT pid, usename, state, query_start
FROM pg_stat_activity
WHERE datname = 'changemaker_v2' AND state = 'idle';

-- Kill specific connection
SELECT pg_terminate_backend(PID_HERE);

-- Kill all idle connections (careful!)
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = 'changemaker_v2' AND state = 'idle';

Solution 3: Adjust connection pool

In api/prisma/schema.prisma:

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  // Add connection pool config
  // connectionLimit = 10  // Default
}

Or via DATABASE_URL:

DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=20"

Solution 4: Restart API

# Restart releases all connections
docker compose restart api

# Check if connections cleared
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
  -c "SELECT count(*) FROM pg_stat_activity WHERE datname = 'changemaker_v2';"

Solution 5: Increase PostgreSQL max connections

In docker-compose.yml:

v2-postgres:
  # ...
  command: postgres -c max_connections=200

Then restart:

docker compose up -d v2-postgres

Prevention

  • Proper cleanup - Always close Prisma clients
  • Connection pooling - Use appropriate pool size
  • Monitor connections - Alert on high usage
  • Query optimization - Reduce long-running queries

Unique Constraint Violation

Severity: 🟡 Medium

Symptoms

Error: Unique constraint failed on the fields: (`email`)

Or:

PrismaClientKnownRequestError:
Unique constraint failed on the constraint: `User_email_key`

Common Causes

  1. Duplicate email - User already exists
  2. Race condition - Two creates at same time
  3. Case sensitivity - Email differs only in case
  4. Retry logic - Request sent multiple times

Solutions

Solution 1: Check existing records

# Find duplicate
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
  -c "SELECT id, email, \"createdAt\" FROM \"User\" WHERE email = 'duplicate@example.com';"

Solution 2: Update instead of create

// Instead of:
await prisma.user.create({ data: { email, ... } });

// Use upsert:
await prisma.user.upsert({
  where: { email },
  update: { name, ... },
  create: { email, name, ... }
});

Solution 3: Handle error gracefully

try {
  await prisma.user.create({ data });
} catch (error) {
  if (error.code === 'P2002') {
    // Unique constraint violation
    const field = error.meta?.target?.[0];
    throw new Error(`${field} already exists`);
  }
  throw error;
}

Solution 4: Delete duplicate

# If truly duplicate, delete one
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
  -c "DELETE FROM \"User\" WHERE id = 'ID_TO_DELETE';"

Prevention

  • Check before create - Query first to check existence
  • Use upsert - Update or create atomically
  • Unique indexes - Database enforces uniqueness
  • Case normalization - Store emails lowercase

Foreign Key Constraint

Severity: 🟡 Medium

Symptoms

Error: Foreign key constraint failed on the field: `campaignId`

Or:

Error: An operation failed because it depends on one or more records that were required but not found. Record to update not found.

Common Causes

  1. Parent doesn't exist - Referenced record missing
  2. Wrong ID - Typo in foreign key value
  3. Delete order - Trying to delete parent before children
  4. Null constraint - Foreign key required but null provided

Solutions

Solution 1: Verify parent exists

# Check campaign exists
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
  -c "SELECT id, name FROM \"Campaign\" WHERE id = 'CAMPAIGN_ID';"

Solution 2: Create parent first

// Create campaign first
const campaign = await prisma.campaign.create({
  data: { name: 'My Campaign', ... }
});

// Then create email with campaignId
const email = await prisma.campaignEmail.create({
  data: {
    campaignId: campaign.id,  // Use created campaign's ID
    ...
  }
});

Solution 3: Delete children first

// Delete all emails in campaign
await prisma.campaignEmail.deleteMany({
  where: { campaignId }
});

// Then delete campaign
await prisma.campaign.delete({
  where: { id: campaignId }
});

// Or use cascade delete in schema:
// @@relation(onDelete: Cascade)

Solution 4: Use transactions

// Ensure atomicity
await prisma.$transaction([
  prisma.campaignEmail.deleteMany({ where: { campaignId } }),
  prisma.campaign.delete({ where: { id: campaignId } })
]);

Prevention

  • Cascade deletes - Configure in schema where appropriate
  • Soft deletes - Flag as deleted instead of removing
  • Validation - Check foreign keys exist before creating
  • Transactions - Use for multi-step operations

Frontend Errors

Network Error

Severity: 🟠 High

Symptoms

Browser console:

Error: Network Error

Or:

AxiosError: Request failed with status code undefined

User sees: API request fails, loading spinner never stops.

Common Causes

  1. API down - API container not running
  2. Wrong API URL - VITE_API_URL misconfigured
  3. CORS issue - Browser blocking request
  4. Network timeout - Request taking too long

Solutions

Solution 1: Check API status

# Is API running?
docker compose ps api

# Check API logs
docker compose logs api --tail=50

# Test API directly
curl http://localhost:4000/api/health

Solution 2: Verify API URL

# Check admin .env
cat admin/.env

# Should have:
VITE_API_URL=http://localhost:4000

# In Docker, use:
VITE_API_URL=http://api:4000

Solution 3: Check browser console

Press F12, check:

  • Network tab - Does request appear? What's the status?
  • Console tab - Any CORS errors?

Solution 4: Test from different client

# From command line
curl http://localhost:4000/api/campaigns

# If this works but browser doesn't, it's a CORS issue

Prevention

  • Health checks - Monitor API availability
  • Error boundaries - Catch and display network errors
  • Retry logic - Auto-retry failed requests
  • Offline detection - Detect and handle offline state

CORS Errors

Severity: 🟠 High

Symptoms

Browser console:

Access to XMLHttpRequest at 'http://localhost:4000/api/users' from origin
'http://localhost:3000' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.

Common Causes

  1. Missing CORS config - API not configured for CORS
  2. Wrong origin - Admin URL not in allowed origins
  3. Credentials flag - withCredentials set but not allowed
  4. Preflight failure - OPTIONS request failing

Solutions

Solution 1: Check API CORS configuration

In api/src/server.ts:

app.use(cors({
  origin: process.env.CORS_ORIGINS?.split(',') || ['http://localhost:3000'],
  credentials: true
}));

Solution 2: Verify CORS_ORIGINS

# Check .env
cat .env | grep CORS_ORIGINS

# Should include admin URL:
CORS_ORIGINS=http://localhost:3000,https://app.cmlite.org

Solution 3: Add origin temporarily

For development:

# In .env
CORS_ORIGINS=*  # Allow all origins (dev only!)

# Restart API
docker compose restart api

Solution 4: Check preflight request

In browser Network tab:

  1. Find OPTIONS request before actual request
  2. Check if it returns 200 OK
  3. Check response headers include:
    • Access-Control-Allow-Origin
    • Access-Control-Allow-Methods
    • Access-Control-Allow-Headers

Prevention

  • Explicit origins - List all allowed origins
  • Environment-based - Different origins per environment
  • Credentials support - Enable if using cookies/auth
  • Preflight caching - Cache OPTIONS responses

!!! warning "Security Note" Never use CORS_ORIGINS=* in production with credentials enabled.


Module Not Found

Severity: 🟡 Medium

Symptoms

Error: Cannot find module '@/components/MyComponent'

Or:

Module not found: Can't resolve 'some-package'

Common Causes

  1. Missing dependency - Package not installed
  2. Wrong import path - Typo in path
  3. Path alias issue - @ alias not configured
  4. Case sensitivity - Wrong case in filename

Solutions

Solution 1: Install missing package

cd admin

# Install package
npm install some-package

# Or if dev dependency
npm install -D some-package

# Restart dev server
npm run dev

Solution 2: Check import path

// Wrong:
import MyComponent from '@/Component/MyComponent';

// Right:
import MyComponent from '@/components/MyComponent';

// Verify file exists:
// admin/src/components/MyComponent.tsx

Solution 3: Verify path alias

In admin/vite.config.ts:

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  }
});

In admin/tsconfig.json:

{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

Solution 4: Clear cache and reinstall

cd admin

# Remove node_modules and lock file
rm -rf node_modules package-lock.json

# Reinstall
npm install

# Restart
npm run dev

Prevention

  • Type checking - Use TypeScript for import validation
  • IDE support - Configure path aliases in IDE
  • Linting - Use ESLint with import plugin
  • Documentation - Document custom path aliases

Hydration Errors

Severity: 🟡 Medium

Symptoms

Browser console:

Warning: Text content did not match. Server: "..." Client: "..."

Or:

Error: Hydration failed because the initial UI does not match what was
rendered on the server.

Common Causes

  1. Date formatting - Server/client timezone difference
  2. Random values - Using Math.random() or uuid
  3. localStorage - Reading from localStorage during render
  4. User agent - Checking window.navigator during SSR
  5. Third-party scripts - Injected by browser extensions

Solutions

Solution 1: Use useEffect for client-only code

// Wrong:
const Component = () => {
  const value = localStorage.getItem('key');
  return <div>{value}</div>;
};

// Right:
const Component = () => {
  const [value, setValue] = useState<string | null>(null);

  useEffect(() => {
    setValue(localStorage.getItem('key'));
  }, []);

  return <div>{value}</div>;
};

Solution 2: Consistent date formatting

// Wrong:
<div>{new Date().toLocaleString()}</div>  // Varies by locale

// Right:
import dayjs from 'dayjs';
<div>{dayjs().format('YYYY-MM-DD HH:mm:ss')}</div>

Solution 3: suppressHydrationWarning for known mismatches

// For values that intentionally differ (like timestamps)
<time suppressHydrationWarning>
  {new Date().toISOString()}
</time>

Solution 4: Check browser extensions

Disable browser extensions temporarily to see if error persists.

Prevention

  • Avoid client-only APIs during render - Use useEffect
  • Consistent formatting - Same format server and client
  • Test without extensions - Regular testing
  • React DevTools - Use to identify mismatches

!!! note "Changemaker Lite V2" Current admin is CSR (Client-Side Rendered) only, so hydration errors shouldn't occur. This section is for future SSR/SSG implementations.


File Upload Errors

File Too Large

Severity: 🟡 Medium

Symptoms

{
  "error": "Payload Too Large",
  "message": "File size exceeds maximum of 10485760 bytes"
}

Or browser:

Request Entity Too Large

Common Causes

  1. File exceeds limit - Video larger than 10GB
  2. Nginx limit - Reverse proxy blocking
  3. Wrong content type - Not multipart/form-data
  4. Network timeout - Upload taking too long

Solutions

Solution 1: Check file size

// Before upload
const file = event.target.files[0];
const maxSize = 10 * 1024 * 1024 * 1024; // 10GB

if (file.size > maxSize) {
  alert(`File too large. Max size: ${maxSize / 1024 / 1024 / 1024}GB`);
  return;
}

Solution 2: Increase limits

In api/src/modules/media/routes/upload.routes.ts:

fastify.register(multipart, {
  limits: {
    fileSize: 10 * 1024 * 1024 * 1024  // 10GB
  }
});

In nginx/conf.d/api.conf:

client_max_body_size 10G;

Solution 3: Use chunked upload

For very large files, implement resumable upload:

// TODO: Implement chunked upload in Phase 15

Solution 4: Compress video

# Before uploading, compress with ffmpeg
ffmpeg -i input.mp4 -c:v libx264 -crf 23 -c:a aac output.mp4

Prevention

  • Client validation - Check size before upload
  • Progress indicator - Show upload progress
  • Compression - Compress large videos
  • Chunked uploads - For files > 1GB

Invalid File Type

Severity: 🟢 Low

Symptoms

{
  "error": "Bad Request",
  "message": "Invalid file type. Allowed: mp4, mov, avi, mkv, webm, m4v, flv"
}

Common Causes

  1. Wrong extension - File has unsupported extension
  2. Missing extension - Filename has no extension
  3. Mismatched extension - Extension doesn't match content
  4. MIME type issue - Browser sends wrong MIME type

Solutions

Solution 1: Check supported formats

Supported video formats:

  • MP4 (.mp4)
  • MOV (.mov)
  • AVI (.avi)
  • MKV (.mkv)
  • WebM (.webm)
  • M4V (.m4v)
  • FLV (.flv)

Solution 2: Convert video

# Convert to MP4 (most compatible)
ffmpeg -i input.avi -c:v libx264 -c:a aac output.mp4

Solution 3: Check file extension

const file = event.target.files[0];
const ext = file.name.split('.').pop().toLowerCase();
const allowed = ['mp4', 'mov', 'avi', 'mkv', 'webm', 'm4v', 'flv'];

if (!allowed.includes(ext)) {
  alert(`Invalid file type: .${ext}`);
  return;
}

Solution 4: Verify with file command

# Check actual file type
file video.mp4

# Should show:
# video.mp4: ISO Media, MP4 v2 [ISO 14496-14]

Prevention

  • Client validation - Check extension before upload
  • MIME type checking - Validate content type
  • File magic numbers - Check file signature
  • Clear documentation - List supported formats

Upload Timeout

Severity: 🟡 Medium

Symptoms

Error: timeout of 30000ms exceeded

Or:

504 Gateway Timeout

Common Causes

  1. Slow network - Large file, slow connection
  2. Server timeout - Request timeout too short
  3. Processing delay - FFprobe taking too long
  4. Network interruption - Connection dropped

Solutions

Solution 1: Increase timeout

In admin/src/lib/media-api.ts:

export const mediaApi = axios.create({
  baseURL: import.meta.env.VITE_MEDIA_API_URL,
  timeout: 300000  // 5 minutes instead of 30 seconds
});

Solution 2: Check upload progress

await mediaApi.post('/upload', formData, {
  onUploadProgress: (progressEvent) => {
    const percent = (progressEvent.loaded / progressEvent.total) * 100;
    console.log(`Upload: ${percent.toFixed(2)}%`);
  }
});

Solution 3: Increase nginx timeout

In nginx/conf.d/api.conf:

proxy_read_timeout 300s;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;

Solution 4: Upload via chunks

// TODO: Implement chunked upload for large files

Prevention

  • Progress indicator - Show upload progress
  • Generous timeouts - Allow enough time for large files
  • Retry logic - Auto-retry on network errors
  • Chunked uploads - For files > 1GB

Email Errors

SMTP Connection Failed

Severity: 🔴 Critical

Symptoms

API logs:

Error: Connection timeout
Error: connect ECONNREFUSED 127.0.0.1:587

Or:

Error: Invalid login: 535-5.7.8 Username and Password not accepted

Common Causes

  1. SMTP server down - Mail server unreachable
  2. Wrong credentials - Invalid username/password
  3. Port blocked - Firewall blocking SMTP port
  4. TLS/SSL issue - Certificate validation failed

Solutions

Solution 1: Test SMTP connection

# Test with telnet
telnet smtp.gmail.com 587

# Should connect and show:
# 220 smtp.gmail.com ESMTP...

Solution 2: Verify SMTP configuration

# Check .env
cat .env | grep SMTP

# Required settings:
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
SMTP_FROM=your-email@gmail.com

Solution 3: Use test mode

# In .env
EMAIL_TEST_MODE=true

# Restart API
docker compose restart api

# Emails now sent to MailHog (http://localhost:8025)

Solution 4: Check Gmail app password

For Gmail:

  1. Enable 2-factor authentication
  2. Generate app password at https://myaccount.google.com/apppasswords
  3. Use app password (not regular password) in SMTP_PASS

Solution 5: Test with curl

# Send test email via API
curl -X POST http://localhost:4000/api/test-email \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "to": "test@example.com",
    "subject": "Test Email",
    "text": "This is a test"
  }'

Prevention

  • Test mode for dev - Use MailHog locally
  • Monitor SMTP health - Alert on connection failures
  • Fallback providers - Configure backup SMTP server
  • Queue system - BullMQ retries failed emails

Template Not Found

Severity: 🟠 High

Symptoms

API logs:

Error: Email template not found: campaign-email

Or:

Error: ENOENT: no such file or directory, open 'templates/campaign-email.html'

Common Causes

  1. Missing template file - Template not created
  2. Wrong template name - Typo in template name
  3. Wrong path - Looking in wrong directory
  4. Deleted template - Template was removed

Solutions

Solution 1: Check template exists

# List all templates
docker compose exec api ls -la templates/

# Should show:
# campaign-email.html
# shift-confirmation.html
# verification-email.html
# etc.

Solution 2: Verify template name

In api/src/services/email.service.ts:

// Template names must match filenames (without .html)
await emailService.sendEmail({
  to: email,
  subject: 'Campaign Email',
  template: 'campaign-email',  // Looks for templates/campaign-email.html
  variables: { ... }
});

Solution 3: Create missing template

# Create template
docker compose exec api sh -c 'cat > templates/my-template.html << EOF
<!DOCTYPE html>
<html>
<body>
  <h1>Hello {{name}}</h1>
  <p>{{message}}</p>
</body>
</html>
EOF'

Solution 4: Use email template system

# Navigate to admin UI
http://localhost:3000/app/email-templates

# Create template there (saved to database + file)

Prevention

  • Seed templates - Include in database seed
  • Template management - Use admin UI to manage
  • Version control - Keep templates in git
  • Validation - Check template exists before sending

Variable Missing

Severity: 🟡 Medium

Symptoms

Email received with placeholders not replaced:

Hello {{name}},
Your campaign {{campaignName}} is ready.

Or API logs:

Warning: Template variable 'campaignName' not provided

Common Causes

  1. Variable not passed - Missing from variables object
  2. Variable name mismatch - Typo in variable name
  3. Wrong template - Using wrong template
  4. Case sensitivity - Variable name case mismatch

Solutions

Solution 1: Check template variables

In template file:

<!-- templates/campaign-email.html -->
<h1>Hello {{firstName}}</h1>
<p>Your campaign "{{campaignName}}" is ready.</p>
<p>Visit: {{campaignUrl}}</p>

Solution 2: Provide all variables

await emailService.sendEmail({
  to: email,
  subject: 'Campaign Ready',
  template: 'campaign-email',
  variables: {
    firstName: user.name.split(' ')[0],
    campaignName: campaign.name,
    campaignUrl: `${process.env.PUBLIC_URL}/campaigns/${campaign.id}`
  }
});

Solution 3: Use default values

<!-- In template, provide fallback -->
<h1>Hello {{firstName || 'Friend'}}</h1>

Solution 4: Validate before sending

// Check all required variables exist
const required = ['firstName', 'campaignName', 'campaignUrl'];
const missing = required.filter(key => !variables[key]);

if (missing.length > 0) {
  throw new Error(`Missing template variables: ${missing.join(', ')}`);
}

Prevention

  • Template validation - Check variables on save
  • TypeScript types - Type template variables
  • Documentation - Document required variables
  • Default values - Provide sensible defaults

Quick Reference Table

Error Code/Message Category Common Cause Quick Fix Severity
401 Unauthorized Auth Token expired Re-login 🟠
403 Forbidden Auth Wrong role Check user role 🟠
404 Not Found API Wrong URL/ID Verify resource exists 🟢
422 Unprocessable Validation Constraint violation Check validation details 🟡
500 Server Error API Code bug Check API logs 🔴
ECONNREFUSED Database DB not running Start database 🔴
Too many connections Database Connection leak Restart API 🟠
Unique constraint Database Duplicate record Use upsert or different value 🟡
Foreign key constraint Database Parent missing Create parent first 🟡
Network Error Frontend API down Check API status 🟠
CORS Error Frontend Origin not allowed Add to CORS_ORIGINS 🟠
Module not found Frontend Missing package npm install 🟡
File too large Upload Exceeds 10GB Compress or increase limit 🟡
Invalid file type Upload Wrong format Convert to MP4 🟢
Upload timeout Upload Slow network Increase timeout 🟡
SMTP failed Email Wrong credentials Check SMTP config 🔴
Template not found Email Missing file Create template 🟠
Variable missing Email Not provided Add to variables object 🟡

When to Report Bugs

Report These

Unexpected behavior - System does something wrong

  • 500 errors (unless caused by your config)
  • Data corruption
  • Security vulnerabilities
  • Performance regressions

Missing features - Documented feature doesn't work

  • API endpoint returns 404 but is documented
  • UI button does nothing
  • Feature flag doesn't enable feature

Unclear documentation - Can't figure out how to do something

  • Documentation contradicts actual behavior
  • Missing setup steps
  • Confusing error messages

Don't Report These

Configuration errors - Your setup is wrong

  • Missing .env variables
  • Wrong database credentials
  • Port conflicts

Environment issues - Your system is incompatible

  • Old Docker version
  • Missing dependencies
  • Network restrictions

User errors - Misunderstanding how to use

  • Wrong API endpoint used
  • Invalid data format
  • Permission errors from lack of role

How to Report

  1. Check this troubleshooting guide first
  2. Search existing GitHub issues
  3. If new, create issue with:
    • Clear title describing problem
    • Steps to reproduce
    • Expected vs actual behavior
    • Relevant logs (sanitize sensitive data)
    • System information (Docker version, OS, etc.)

General Documentation

Specific Troubleshooting

Support


Last Updated: February 2026 Version: V2.0 Status: Complete