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 |