Skip to content

Error Handling & Status Codes

Status: Final Version: 1.0 Last Updated: 2026-01-03


Purpose

Define the standard error response model, error codes, HTTP status codes, and error handling patterns for all ESG platform APIs.


Standard Error Response

Schema:

{
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable error message",
    "details": {}, // Optional: field-level errors or context
    "request_id": "req_abc123",
    "timestamp": "2026-01-03T14:30:00Z"
  }
}

Example - Validation Error:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The given data was invalid.",
    "details": {
      "value": ["The value must be a number.", "The value must be at least 0."],
      "evidence_files": ["At least one evidence file is required."]
    },
    "request_id": "req_550e8400",
    "timestamp": "2026-01-03T14:30:00Z"
  }
}


HTTP Status Codes

Status Code Usage
Success
OK 200 Successful GET, PUT, PATCH
Created 201 Successful POST (resource created)
Accepted 202 Async job queued
No Content 204 Successful DELETE
Client Errors
Bad Request 400 Malformed JSON, invalid parameters
Unauthorized 401 Missing/invalid/expired token
Forbidden 403 Valid token but insufficient permissions
Not Found 404 Resource doesn't exist
Conflict 409 Duplicate submission, state conflict
Unprocessable Entity 422 Validation failed
Too Many Requests 429 Rate limit exceeded
Server Errors
Internal Server Error 500 Unexpected error
Service Unavailable 503 Maintenance mode, queue full

Error Codes

Authentication & Authorization (AUTH_*)

Code HTTP Status Description Action
AUTH_TOKEN_MISSING 401 No Authorization header Provide JWT token
AUTH_TOKEN_INVALID 401 Malformed or tampered token Re-authenticate
AUTH_TOKEN_EXPIRED 401 Token past expiry Use refresh token
AUTH_INSUFFICIENT_PERMISSIONS 403 User role lacks permission Contact admin for access
AUTH_TENANT_MISMATCH 403 Token tenant ≠ X-Tenant-Id header Fix tenant context
AUTH_SCOPE_VIOLATION 403 User lacks site/project access Request access from admin

Validation (VALIDATION_*)

Code HTTP Status Description Details Field
VALIDATION_ERROR 422 Field validation failed Field-level errors
VALIDATION_RULE_FAILED 422 Business rule violated Rule name + details
VALIDATION_EVIDENCE_MISSING 422 Required evidence not attached Required evidence types
VALIDATION_ANOMALY_DETECTED 200 (warning) Outlier detected Warning message

Resource Errors (RESOURCE_*)

Code HTTP Status Description
RESOURCE_NOT_FOUND 404 Entity doesn't exist or user lacks access
RESOURCE_ALREADY_EXISTS 409 Duplicate resource (e.g., submission with same UUID)
RESOURCE_LOCKED 409 Reporting period locked, no edits allowed
RESOURCE_DELETED 410 Resource was soft-deleted

State Errors (STATE_*)

Code HTTP Status Description Example
STATE_TRANSITION_INVALID 409 Illegal state transition Can't approve a rejected submission
STATE_PREREQUISITE_MISSING 409 Prerequisite not met Can't lock period with unreviewed submissions

Rate Limiting (RATE_*)

Code HTTP Status Description Headers
RATE_LIMIT_EXCEEDED 429 Too many requests X-RateLimit-Reset, Retry-After

System Errors (SYSTEM_*)

Code HTTP Status Description
SYSTEM_ERROR 500 Unexpected internal error
SYSTEM_MAINTENANCE 503 Scheduled maintenance
SYSTEM_DATABASE_ERROR 503 Database unavailable
SYSTEM_QUEUE_FULL 503 Background queue at capacity

Quarkus JAX-RS Implementation

Exception Mappers

// src/main/kotlin/exception/GlobalExceptionMapper.kt
import jakarta.ws.rs.core.Response
import jakarta.ws.rs.ext.ExceptionMapper
import jakarta.ws.rs.ext.Provider
import jakarta.inject.Inject
import jakarta.validation.ConstraintViolationException
import io.quarkus.security.ForbiddenException
import io.quarkus.security.UnauthorizedException
import java.time.Instant

// Generic exception mapper for all unhandled exceptions
@Provider
class GlobalExceptionMapper @Inject constructor(
    private val requestIdProvider: RequestIdProvider
) : ExceptionMapper<Exception> {

    override fun toResponse(ex: Exception): Response {
        val status: Int
        val code: String
        val message: String
        val details: Map<String, List<String>>?

        when (ex) {
            is ConstraintViolationException -> {
                status = 422
                code = "VALIDATION_ERROR"
                message = "The given data was invalid."
                details = ex.constraintViolations.groupBy(
                    { it.propertyPath.toString() },
                    { it.message }
                )
            }
            is ForbiddenException -> {
                status = 403
                code = "AUTH_INSUFFICIENT_PERMISSIONS"
                message = ex.message ?: "Access denied"
                details = null
            }
            is UnauthorizedException -> {
                status = 401
                code = "AUTH_TOKEN_INVALID"
                message = ex.message ?: "Authentication failed"
                details = null
            }
            is EntityNotFoundException -> {
                status = 404
                code = "RESOURCE_NOT_FOUND"
                message = "The requested resource was not found."
                details = null
            }
            is WebApplicationException -> {
                val response = ex.response
                status = response.status
                code = getErrorCodeForStatus(status)
                message = ex.message ?: Response.Status.fromStatusCode(status)?.reasonPhrase ?: "Error"
                details = null
            }
            else -> {
                status = 500
                code = "SYSTEM_ERROR"
                message = "An unexpected error occurred."
                details = null
            }
        }

        val errorResponse = ErrorResponse(
            error = ErrorDetails(
                code = code,
                message = message,
                details = details,
                requestId = requestIdProvider.getRequestId(),
                timestamp = Instant.now().toString()
            )
        )

        return Response.status(status)
            .entity(errorResponse)
            .build()
    }

    private fun getErrorCodeForStatus(status: Int): String {
        return when (status) {
            400 -> "VALIDATION_ERROR"
            401 -> "AUTH_TOKEN_INVALID"
            403 -> "AUTH_INSUFFICIENT_PERMISSIONS"
            404 -> "RESOURCE_NOT_FOUND"
            409 -> "RESOURCE_CONFLICT"
            429 -> "RATE_LIMIT_EXCEEDED"
            503 -> "SYSTEM_MAINTENANCE"
            else -> "SYSTEM_ERROR"
        }
    }
}

data class ErrorResponse(
    val error: ErrorDetails
)

data class ErrorDetails(
    val code: String,
    val message: String,
    val details: Map<String, List<String>>? = null,
    val requestId: String,
    val timestamp: String
)

Custom Exception Mappers

// src/main/kotlin/exception/StateTransitionException.kt
class StateTransitionException(
    val from: String,
    val to: String,
    val reason: String
) : RuntimeException("Cannot transition from $from to $to: $reason")

// Custom exception mapper for StateTransitionException
@Provider
class StateTransitionExceptionMapper @Inject constructor(
    private val requestIdProvider: RequestIdProvider
) : ExceptionMapper<StateTransitionException> {

    override fun toResponse(ex: StateTransitionException): Response {
        val errorResponse = ErrorResponse(
            error = ErrorDetails(
                code = "STATE_TRANSITION_INVALID",
                message = ex.message ?: "Invalid state transition",
                details = mapOf(
                    "from" to listOf(ex.from),
                    "to" to listOf(ex.to),
                    "reason" to listOf(ex.reason)
                ),
                requestId = requestIdProvider.getRequestId(),
                timestamp = Instant.now().toString()
            )
        )
        return Response.status(409).entity(errorResponse).build()
    }
}

Acceptance Criteria

Done When: - [ ] All API endpoints return standard error JSON - [ ] Field-level validation errors included in details - [ ] Request IDs logged and returned in errors - [ ] HTTP status codes match error scenarios - [ ] Rate limit errors include Retry-After header


Cross-References


Change Log

Version Date Author Changes
1.0 2026-01-03 Senior Product Architect Initial error handling specification