Input and resource controls

Return safe errors

Use consistent status codes and problem details without exposing stack traces, queries, secrets, internal hosts, or authorization-sensitive existence.

API errors are part of the contract. They should guide the client without teaching an attacker about internals. Every stack trace, SQL fragment, or internal hostname in a response is free reconnaissance.

Use a stable, safe shape

Return a stable type, title, status, safe detail, and request ID. The problem details format from RFC 9457 gives you a conventional structure:

{
  "type": "https://api.example.com/errors/validation",
  "title": "Invalid request body",
  "status": 400,
  "detail": "items must contain between 1 and 100 entries",
  "requestId": "req_7f3ab910"
}

Keep private diagnostics in protected logs. A request ID connects the safe response to detailed evidence without copying sensitive diagnostics outside:

# internal log line, never in the response
req_7f3ab910 POST /invoices 400 ValidationError items.length=50000 user=usr_8817

Keep error identifiers stable enough for clients, but do not expose internal exception classes. A type of validation survives a refactor. PostgresUniqueViolationError does not, and it names your database for free.

Don’t confirm what the caller may not know

Returning invoice 193 belongs to another account confirms the record exists. That is an enumeration oracle. Decide deliberately when authorization failures should be indistinguishable from missing resources:

curl -i https://api.example.com/invoices/inv_someone_elses \
  -H "Authorization: Bearer $TOKEN"
# HTTP/1.1 404 Not Found   <- same response as a truly missing invoice

Returning the same public result for missing and forbidden records hides that fact, but protected logs still need the real cause. The security team must be able to distinguish a typo from a probing campaign.

Verify every error path

Trigger each class of failure on purpose and inspect what leaks. A common miss: your handlers return clean problem details, but the framework’s default 500 handler still prints a stack trace when something throws outside your code. Send a request that breaks early — an unparseable body, an oversized header — and read the raw response.

Capture public and internal errors for missing, forbidden, invalid, limited, and failed requests. Prove public responses contain stable codes and request IDs while stack traces and object existence stay private.

Lesson completed

Take this course offline

Get every free book, course edition, and software download.

Get the download library →