✨ Strapi MCP is now Generally Available - let your agents manage your Strapi content ✨

A 500 is a category, not a diagnosis. It means Strapi hit an exception it did not have a specific handler for, and, deliberately, told you nothing about it. Leaking stack traces to API clients is an information disclosure risk, so Strapi returns a generic message in production and writes the real one to the server log.

{
  "data": null,
  "error": {
    "status": 500,
    "name": "InternalServerError",
    "message": "Internal Server Error"
  }
}

The error you need is in your server logs. Everything below is about getting to it efficiently.

Step 1: Read the server log

Not the browser console. Not the network tab. The terminal or log destination where Strapi is running.

# Development
npm run develop

# PM2
pm2 logs strapi --lines 200

# Docker
docker compose logs -f --tail=200 strapi

# systemd
journalctl -u strapi -n 200 -f

# Kubernetes
kubectl logs -f deploy/strapi --tail=200

Reproduce the request while watching. You are looking for a stack trace timestamped at the moment of the failure. The first line names the actual error; the frames underneath tell you whether it originated in your code or inside Strapi.

If there is no stack trace at all, the request may not be reaching Strapi. Check whether your proxy is generating the 500 itself: an Nginx 500 is an HTML page, a Strapi 500 is the JSON above. That distinction alone saves a lot of time.

Step 2: Turn up the detail

If the log is too terse, raise the log level:

export default {
  level: 'debug',
};

Or per-run:

STRAPI_LOG_LEVEL=debug npm run develop

Levels are fatal, error, warn, info, debug, trace. Use debug while you are hunting and put it back afterwards, because debug in production is noisy and can log data you would rather not persist.

To see the SQL Strapi is running, enable the database debug flag temporarily:

export default ({ env }) => ({
  connection: {
    client: 'postgres',
    connection: { /* ... */ },
    debug: true,   // logs every query, development only
  },
});

Step 3: Narrow it down

Reproduce with curl so you eliminate the frontend entirely:

curl -i https://cms.example.com/api/articles \
  -H "Authorization: Bearer $TOKEN"

Then bisect:

  • Does the same route fail with different parameters? ?populate=* failing where a plain call succeeds points at a relation or component.
  • Does it fail for every entry or one? One entry means bad data, not bad code.
  • Does it fail on a fresh local database? If not, it is data-dependent.
  • Did it start after a deploy? git log the range and look at schema and controller changes.
  • Does it fail for one role and not another? Look at policies and lifecycle hooks that branch on the user.

The causes that actually come up

An unhandled exception in your own code

Custom controllers, services, lifecycle hooks, and middleware are the most common source. Anything that throws and is not caught becomes a 500.

import { factories } from '@strapi/strapi';

export default factories.createCoreController('api::article.article', ({ strapi }) => ({
  async findBySlug(ctx) {
    const { slug } = ctx.params;

    const entries = await strapi.documents('api::article.article').findMany({
      filters: { slug },
      limit: 1,
    });

    // Without this guard, the line below throws on an unknown slug
    if (!entries.length) {
      return ctx.notFound('Article not found');
    }

    return this.transformResponse(entries[0]);
  },
}));

Return the right status deliberately rather than letting an exception decide for you. Strapi provides ctx.notFound(), ctx.badRequest(), ctx.forbidden(), and ctx.unauthorized().

A lifecycle hook throwing

Lifecycle hooks run inside the request, so an error there surfaces as a 500 on an endpoint whose controller is perfectly fine. This is a genuinely misleading failure, because the stack trace points at the model, not the route.

export default {
  async beforeCreate(event) {
    const { data } = event.params;
    // If `data.title` is undefined this throws, and the POST returns 500
    data.slug = data.title.toLowerCase().replace(/\s+/g, '-');
  },
};

Guard your inputs, and wrap anything that calls an external service in try/catch with a decision about whether failure should block the write.

The database connection dropped

If Strapi booted fine and started 500ing later, the database may have gone away, whether a failover, a restart, or an idle connection killed by a proxy or by Docker.

Look for ECONNRESET, Connection terminated unexpectedly, or Knex: Timeout acquiring a connection in the log. In Docker or serverless, set pool.min: 0 so the pool does not hand out dead sockets. See the Knex pool guide.

A misconfigured email or upload provider

Sending an email or uploading a file through a provider with missing credentials throws inside the plugin. The endpoint that fails, such as /api/auth/forgot-password or /api/upload, is a useful clue that a provider is involved.

Check the provider configuration in config/plugins and confirm the environment variables exist in the environment that is failing.

A content-type or component mismatch

If Strapi 500s on every request to one content type, its schema may reference a component or relation that does not exist. That usually prevents boot entirely, but a partially broken relation can survive startup and fail at query time. See Cannot read properties of undefined (reading 'attributes').

A request that is not what you think it is

The last reply in the original forum thread is a good example: a frontend was posting plain text to a login endpoint without Content-Type: application/json, so Strapi received an unparsed body and threw when it tried to read fields off it.

await fetch('https://cms.example.com/api/auth/local', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },   // easy to forget
  body: JSON.stringify({ identifier, password }),
});

Check the raw request before you go looking in the backend. A missing header, a stringified body sent as an object, or a form-encoded payload where JSON is expected all produce 500s that look like server bugs.

Missing environment variables

APP_KEYS, JWT_SECRET, ADMIN_JWT_SECRET, API_TOKEN_SALT, and ENCRYPTION_KEY are required. A missing JWT_SECRET typically 500s specifically on authentication endpoints while everything else works, which is a confusing signature until you know it.

Getting more from errors long-term

Customise error handling

You can intercept errors globally to log richer context:

export default [
  'strapi::logger',
  {
    name: 'strapi::errors',
    config: {
      // Never enable this in production: it exposes stack traces to clients
      // handler: ...
    },
  },
  // ...
];

See error handling for the supported customisation points.

pm2 logs is fine until the incident is three days old. Send Strapi's output to a log aggregator such as Better Stack, Datadog, Grafana Loki, or CloudWatch, so you can search by timestamp and correlate with a deploy.

Add error tracking

Sentry, Bugsnag, or similar gives you the stack trace, the request, and the user, without needing to reproduce. Register it in src/index:

import * as Sentry from '@sentry/node';

export default {
  register({ strapi }) {
    if (process.env.SENTRY_DSN) {
      Sentry.init({
        dsn: process.env.SENTRY_DSN,
        environment: process.env.NODE_ENV,
      });
    }
  },
  bootstrap() {},
};

Make health visible

Poll /_health from an uptime monitor so you learn about a down instance from an alert rather than from a colleague. See the health check guide.

What not to do

Generic web-server advice (clear your browser cache, try another browser, check file permissions, look at .htaccess) comes from the shared-hosting PHP world and does not apply here. Strapi is a Node process. Nothing the browser does causes a 500, and there is no .htaccess.

Read the server log. The answer is in it.

Checklist

  1. Read the Strapi log, not the browser.
  2. Set level: 'debug' if the trace is thin.
  3. Reproduce with curl to rule out the frontend.
  4. Identify whether the frame is in your code or Strapi's.
  5. Check whether it is data-dependent (one entry vs all).
  6. Check whether it started with a deploy.
  7. Verify environment variables exist in that environment.
  8. Check the database is still reachable.

Further reading

Theodore Kelechukwu OnyejiakuDevRel and Community | Software Developer | Technical Writer

Theodore is a Technical Writer and a full-stack software developer. He loves writing technical articles, building solutions, and sharing his expertise.

Seeding Data in Strapi: Why Migrations are the Wrong Tool
EcosystemIntermediate·7 min read

Seeding Data in Strapi: Why Migrations are the Wrong Tool

Migrations run before Strapi's schema sync, so seeding a brand-new content type fails on the missing table. Use bootstrap instead, idempotently.

·September 9, 2026
Strapi and TypeScript: compiling when there are type errors
EcosystemAdvanced·6 min read

Strapi and TypeScript: compiling when there are type errors

A single type error stops your Strapi dev server. The tsconfig settings that let it build anyway, and when each one is actually appropriate.

·September 9, 2026
"Cannot read properties of undefined (reading 'attributes')" in Strapi
EcosystemIntermediate·6 min read

"Cannot read properties of undefined (reading 'attributes')" in Strapi

Strapi will not boot because a content type references a component or relation that does not exist. How to find the broken reference quickly.

·September 9, 2026