← All Articles
Last updated: 2026-03-30

API Returning 500 Errors? How to Debug REST API Issues Systematically

Systematic REST API debugging guide. CORS errors, 500/502/503 errors, timeouts, authentication failures — with curl examples.

TL;DR

When your REST API returns 500 errors, follow this order: reproduce the issue reliably, isolate the failing component, and trace the request through your stack using logs and curl. Most 500 errors come from unhandled exceptions, database connection failures, or misconfigured environment variables. Check your server logs first — the answer is almost always there.

Prerequisites

Step 1: The Systematic Approach — Reproduce, Isolate, Trace

Reproduce

Before debugging anything, you need a reliable way to trigger the error. Document the exact request that fails:

# Save the failing request so you can replay it
curl -v -X POST https://api.example.com/v1/users \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"name": "Test", "email": "test@example.com"}' \
  2>&1 | tee /tmp/api-debug.log

Ask yourself: Does it fail every time? Only with certain data? Only at certain times? Intermittent failures often point to resource exhaustion or race conditions.

Isolate

Narrow down which component is failing. A typical REST API stack has multiple layers:

Client → DNS → Load Balancer → Reverse Proxy (Nginx) → App Server → Database

Test each layer independently. Can you reach Nginx? Can the app server connect to the database? Use targeted checks:

# Test if Nginx is responding at all
curl -I https://api.example.com/health

# Test the app server directly, bypassing the reverse proxy
curl -I http://localhost:3000/health

# Test database connectivity from the app server
mysql -u appuser -p -h db-host -e "SELECT 1;"
# or for PostgreSQL
psql -U appuser -h db-host -c "SELECT 1;"

Trace

Follow the request through every layer. Enable verbose logging temporarily and use request IDs if your application supports them:

# Add a custom request ID to trace through logs
curl -v -X GET https://api.example.com/v1/orders \
  -H "X-Request-ID: debug-12345" \
  -H "Authorization: Bearer YOUR_TOKEN"

Then search your logs for that ID across all services.

Step 2: Reading Server Logs

Logs are your primary debugging tool. Where they live depends on your setup:

systemd Services (journalctl)

# View logs for a specific service, most recent first
journalctl -u my-api-service -n 100 --no-pager

# Follow logs in real-time (like tail -f)
journalctl -u my-api-service -f

# Filter by time range
journalctl -u my-api-service --since "2024-01-15 14:00" --until "2024-01-15 15:00"

# Show only errors
journalctl -u my-api-service -p err -n 50

Docker Containers

# View last 200 lines of a container's logs
docker logs --tail 200 my-api-container

# Follow logs in real-time with timestamps
docker logs -f --timestamps my-api-container

# For docker-compose setups
docker-compose logs -f --tail=100 api

PM2 (Node.js)

# View logs for a specific app
pm2 logs my-api --lines 100

# View only error logs
pm2 logs my-api --err --lines 50

# Flush and start fresh
pm2 flush my-api

Traditional Log Files

# Nginx error log
tail -f /var/log/nginx/error.log

# Nginx access log — filter for 500 errors
grep ' 500 ' /var/log/nginx/access.log | tail -20

# Application logs (common locations)
tail -f /var/log/myapp/error.log
tail -f /var/log/syslog | grep myapp

Step 3: Testing with curl

curl is the most versatile tool for API debugging. Master these patterns:

Basic Requests with Verbose Output

# GET request with full headers shown (-v for verbose)
curl -v https://api.example.com/v1/users

# Only show response headers
curl -I https://api.example.com/v1/users

# Show response headers AND body
curl -i https://api.example.com/v1/users

POST Requests with JSON Data

# POST with JSON body
curl -X POST https://api.example.com/v1/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Jane Doe", "email": "jane@example.com", "role": "admin"}'

# POST with data from a file
curl -X POST https://api.example.com/v1/import \
  -H "Content-Type: application/json" \
  -d @payload.json

# PUT request to update a resource
curl -X PUT https://api.example.com/v1/users/42 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOi..." \
  -d '{"name": "Jane Smith"}'

Debugging Connection Issues

# Show timing information
curl -o /dev/null -s -w "DNS: %{time_namelookup}s\nConnect: %{time_connect}s\nTLS: %{time_appconnect}s\nFirstByte: %{time_starttransfer}s\nTotal: %{time_total}s\nHTTP Code: %{http_code}\n" https://api.example.com/v1/health

# Resolve to a specific IP (bypass DNS)
curl --resolve api.example.com:443:10.0.0.5 https://api.example.com/v1/health

# Set a custom timeout (5 seconds)
curl --max-time 5 --connect-timeout 3 https://api.example.com/v1/slow-endpoint

Step 4: Understanding HTTP Status Codes

Each status code tells a specific story. Here is what they mean in practice:

400 Bad Request

Meaning: The server cannot process the request due to malformed syntax or invalid data.

Common causes: Missing required fields, wrong data types, invalid JSON syntax, exceeding field length limits.

# This will likely return 400 — invalid JSON
curl -X POST https://api.example.com/v1/users \
  -H "Content-Type: application/json" \
  -d '{name: "missing quotes on key"}'

401 Unauthorized

Meaning: Authentication is missing or invalid. The server does not know who you are.

Common causes: Missing Authorization header, expired token, wrong API key.

403 Forbidden

Meaning: Authentication succeeded, but you lack permission for this action. The server knows who you are but you are not allowed.

Common causes: Insufficient role/scope, IP allowlist restrictions, resource owned by another user.

404 Not Found

Meaning: The requested resource does not exist at this URL.

Common causes: Typo in URL, resource was deleted, wrong API version prefix, missing trailing slash.

500 Internal Server Error

Meaning: The server encountered an unexpected condition. This is always a server-side bug.

Common causes: Unhandled exceptions, null pointer errors, missing environment variables, database connection failures, out-of-memory conditions.

502 Bad Gateway

Meaning: The reverse proxy (Nginx, Apache) received an invalid response from the upstream application server.

Common causes: App server crashed, app server not running, wrong upstream port in proxy config.

503 Service Unavailable

Meaning: The server is temporarily unable to handle requests.

Common causes: Server is starting up, maintenance mode, overloaded, dependency (database) is down.

504 Gateway Timeout

Meaning: The reverse proxy timed out waiting for the upstream server to respond.

Common causes: Slow database queries, external API calls hanging, insufficient proxy timeout settings.

Step 5: Debugging CORS Errors

Cross-Origin Resource Sharing (CORS) errors only occur in browsers. If your API works in curl but fails in the browser with "CORS policy" errors, the fix is on the server side.

How to Identify CORS Issues

# Simulate a CORS preflight request
curl -v -X OPTIONS https://api.example.com/v1/users \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type, Authorization"

# Check the response for these headers:
# Access-Control-Allow-Origin: https://app.example.com
# Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
# Access-Control-Allow-Headers: Content-Type, Authorization

Nginx CORS Fix

# /etc/nginx/conf.d/api.conf
server {
    listen 443 ssl;
    server_name api.example.com;

    location /v1/ {
        # CORS headers
        add_header Access-Control-Allow-Origin "https://app.example.com" always;
        add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
        add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Request-ID" always;
        add_header Access-Control-Max-Age 86400 always;

        # Handle preflight requests
        if ($request_method = OPTIONS) {
            return 204;
        }

        proxy_pass http://localhost:3000;
    }
}

Express.js CORS Fix

const cors = require('cors');

app.use(cors({
  origin: ['https://app.example.com', 'https://staging.example.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
  credentials: true,
  maxAge: 86400
}));

Important: Never use Access-Control-Allow-Origin: * together with credentials: true. Browsers will reject this combination. Always specify the exact origin.

Step 6: Timeout Issues

Timeouts can occur at every layer. Identify where the timeout happens:

# Measure response time breakdown
curl -w "\n---Timing---\nDNS:        %{time_namelookup}s\nConnect:    %{time_connect}s\nTLS:        %{time_appconnect}s\nFirstByte:  %{time_starttransfer}s\nTotal:      %{time_total}s\n" \
  -o /dev/null -s https://api.example.com/v1/reports/generate

If time_starttransfer is high but time_connect is low, the server is slow to process. If time_connect is high, there is a network or DNS issue.

Common Timeout Configurations

# Nginx — increase proxy timeout for slow endpoints
location /v1/reports/ {
    proxy_pass http://localhost:3000;
    proxy_read_timeout 120s;
    proxy_connect_timeout 10s;
    proxy_send_timeout 60s;
}

# Node.js — increase server timeout
const server = app.listen(3000);
server.setTimeout(120000); // 120 seconds

Step 7: JWT and OAuth Debugging

Authentication issues are among the most common API problems. Here is how to inspect tokens:

Inspecting a JWT Token

A JWT has three parts separated by dots: header.payload.signature. You can decode the first two parts without the secret:

# Decode a JWT payload in the terminal (no secret needed)
echo 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjQyNjIyfQ.signature' \
  | cut -d'.' -f2 \
  | base64 -d 2>/dev/null | python3 -m json.tool

Check for these common issues:

You can also paste tokens into jwt.io for a visual breakdown (never paste production tokens with sensitive data).

Testing OAuth Flows

# Request a new token using client credentials grant
curl -X POST https://auth.example.com/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "scope=read write"

# Use the returned access_token
curl -H "Authorization: Bearer ACCESS_TOKEN_HERE" \
  https://api.example.com/v1/protected-resource

Step 8: Postman and HTTPie

HTTPie — A Human-Friendly curl Alternative

# Install
brew install httpie    # macOS
apt install httpie     # Ubuntu/Debian

# GET request (automatically pretty-prints JSON)
http GET https://api.example.com/v1/users Authorization:"Bearer TOKEN"

# POST with JSON (no need to specify Content-Type)
http POST https://api.example.com/v1/users name="Jane" email="jane@example.com"

# Show both request and response headers
http -v POST https://api.example.com/v1/users name="Jane"

# Download a response to file
http --download https://api.example.com/v1/export

Postman Tips

Troubleshooting Quick Reference

SymptomLikely CauseFirst Step
500 on all endpointsApp crash, missing env var, DB downCheck application logs
500 on one endpointUnhandled exception in that routeCheck logs for stack trace
502 Bad GatewayApp server not runningsystemctl status myapp
504 Gateway TimeoutSlow query or external callCheck curl timing, DB slow log
CORS error in browserMissing response headersTest with curl OPTIONS request
401 with valid tokenToken expired or wrong audienceDecode JWT, check exp claim
Works in Postman, fails in browserCORS or cookie issueCheck preflight response
Intermittent 500sResource exhaustion, race conditionMonitor memory/CPU, check connection pool

Prevention and Best Practices

Need Expert Help?

Can't find the root cause? I debug it live. €49.

Book Now — €49

100% money-back guarantee

HR

Harald Roessler

Infrastructure Engineer with 20+ years experience. Founder of DSNCON GmbH.