Error HandlingΒΆ
Comprehensive guide to API error responses, HTTP status codes, and error handling patterns in the Spring Security Reference project.
Current security error contractΒΆ
The stateless REST chain returns application/json through a production AuthenticationEntryPoint and AccessDeniedHandler.
| Status | Machine code | Meaning |
|---|---|---|
| 401 | authentication_required | Authentication is absent or invalid |
| 403 | access_denied | Authentication succeeded but permission is insufficient |
Both responses contain status, error, message, and path. They deliberately omit exception messages, stack traces, policy expressions, authorities, and credentials. See LAB-005 for implementation and executable assertions.
π¨ Error Response ArchitectureΒΆ
flowchart TD
A[API Request] --> B{Authentication Valid?}
B -->|No| C[401 Unauthorized]
B -->|Yes| D{Authorization Check}
D -->|Forbidden| E[403 Access Denied]
D -->|Allowed| F{Request Valid?}
F -->|Invalid| G[400 Bad Request]
F -->|Valid| H[Controller Processing]
H --> I{Business Logic}
I -->|Error| J[500 Internal Server Error]
I -->|Success| K[200/201/204 Success]
C --> L[Error Response JSON]
E --> L
G --> L
J --> L
style C fill:#ffcdd2
style E fill:#ffcdd2
style G fill:#ffcdd2
style J fill:#ffcdd2
style K fill:#c8e6c9
style L fill:#fff3e0 π HTTP Status Code ReferenceΒΆ
π’ Success Responses (2xx)ΒΆ
| Status Code | Endpoint Example | Description |
|---|---|---|
| 200 OK | GET /api/user/secure | Successful GET request |
| 201 Created | POST /api/users | Resource created successfully |
| 204 No Content | DELETE /api/users/123 | Successful deletion |
π΄ Client Error Responses (4xx)ΒΆ
| Status Code | Scenario | Description |
|---|---|---|
| 400 Bad Request | Invalid request parameters | Malformed request data |
| 401 Unauthorized | Missing/invalid authentication | Authentication required |
| 403 Forbidden | Insufficient permissions | Authorization denied |
| 404 Not Found | Endpoint doesn't exist | Resource not found |
| 405 Method Not Allowed | Wrong HTTP method | Method not supported |
π‘ Server Error Responses (5xx)ΒΆ
| Status Code | Scenario | Description |
|---|---|---|
| 500 Internal Server Error | Unhandled exception | Server-side error |
| 503 Service Unavailable | Service temporarily down | Maintenance mode |
π Authentication Error ResponsesΒΆ
401 Unauthorized - Missing AuthenticationΒΆ
π Learning Points: - No valid authentication credentials provided - Client must authenticate before accessing resource - WWW-Authenticate header suggests authentication method
401 Unauthorized - Invalid JWT TokenΒΆ
π JWT Token Error Types: - Invalid Signature: Token tampered or wrong secret key - Expired Token: Token past expiration time - Malformed Token: Invalid JWT structure - Missing Claims: Required claims not present
401 Unauthorized - Expired JWT TokenΒΆ
{
"timestamp": "2024-01-15T10:30:00.123Z",
"status": 401,
"error": "Unauthorized",
"message": "JWT token has expired",
"details": {
"tokenError": "JWT expired at 2024-01-15T09:30:00Z",
"tokenStatus": "EXPIRED",
"expiredAt": "2024-01-15T09:30:00Z",
"currentTime": "2024-01-15T10:30:00Z"
},
"path": "/api/user/secure"
}
π Token Refresh Recommendation:
{
"error": "Token expired",
"recommendation": "Please obtain a new token via POST /api/auth/login",
"refreshEndpoint": "/api/auth/login"
}
401 Unauthorized - Invalid Basic AuthΒΆ
π« Authorization Error ResponsesΒΆ
403 Forbidden - Insufficient RoleΒΆ
{
"timestamp": "2024-01-15T10:30:00.123Z",
"status": 403,
"error": "Access Denied",
"message": "You don't have permission to access this resource",
"details": {
"requiredRole": "ROLE_ADMIN",
"userRoles": ["ROLE_USER"],
"resource": "/api/admin/secure",
"action": "GET"
},
"path": "/api/admin/secure"
}
π Learning Points: - User is authenticated but lacks required permissions - Shows required vs. actual roles for debugging - Different from 401 - authentication was successful
403 Forbidden - Method SecurityΒΆ
{
"timestamp": "2024-01-15T10:30:00.123Z",
"status": 403,
"error": "Access Denied",
"message": "Method access denied",
"details": {
"methodSecurity": true,
"expression": "hasRole('ADMIN') and hasPermission(#userId, 'User', 'DELETE')",
"evaluationResult": false,
"failedCondition": "hasPermission(#userId, 'User', 'DELETE')"
},
"path": "/api/users/123"
}
β Request Error ResponsesΒΆ
400 Bad Request - Invalid ParametersΒΆ
{
"timestamp": "2024-01-15T10:30:00.123Z",
"status": 400,
"error": "Bad Request",
"message": "Validation failed for request parameters",
"details": {
"validationErrors": [
{
"field": "username",
"message": "Username is required and cannot be empty",
"rejectedValue": ""
},
{
"field": "password",
"message": "Password is required and cannot be empty",
"rejectedValue": ""
}
]
},
"path": "/api/auth/login"
}
404 Not Found - Endpoint Not ExistΒΆ
405 Method Not AllowedΒΆ
π§ Custom Error Handler ImplementationΒΆ
Global Exception HandlerΒΆ
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorResponse> handleAccessDenied(
AccessDeniedException ex, HttpServletRequest request) {
ErrorResponse error = ErrorResponse.builder()
.timestamp(Instant.now())
.status(HttpStatus.FORBIDDEN.value())
.error("Access Denied")
.message("You don't have permission to access this resource")
.path(request.getRequestURI())
.build();
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
}
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<ErrorResponse> handleAuthenticationException(
AuthenticationException ex, HttpServletRequest request) {
ErrorResponse error = ErrorResponse.builder()
.timestamp(Instant.now())
.status(HttpStatus.UNAUTHORIZED.value())
.error("Unauthorized")
.message("Authentication required")
.path(request.getRequestURI())
.build();
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
}
}
Custom Access Denied HandlerΒΆ
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json");
ErrorResponse error = ErrorResponse.builder()
.timestamp(Instant.now())
.status(403)
.error("Access Denied")
.message("You don't have permission to access this resource")
.path(request.getRequestURI())
.details(Map.of(
"user", request.getRemoteUser(),
"requiredAuthority", extractRequiredAuthority(request),
"userAuthorities", getCurrentUserAuthorities()
))
.build();
ObjectMapper mapper = new ObjectMapper();
response.getWriter().write(mapper.writeValueAsString(error));
}
}
π± Client Error Handling StrategiesΒΆ
JavaScript/TypeScript ClientΒΆ
const apiClient = {
async request(url, options = {}) {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getJwtToken()}`,
...options.headers
}
});
if (!response.ok) {
const errorData = await response.json();
throw new ApiError(errorData);
}
return response.json();
}
};
class ApiError extends Error {
constructor(errorResponse) {
super(errorResponse.message);
this.status = errorResponse.status;
this.error = errorResponse.error;
this.details = errorResponse.details;
this.path = errorResponse.path;
this.timestamp = errorResponse.timestamp;
}
isAuthenticationError() {
return this.status === 401;
}
isAuthorizationError() {
return this.status === 403;
}
isTokenExpiredError() {
return this.status === 401 &&
this.details?.tokenStatus === 'EXPIRED';
}
}
// Usage example
try {
const userData = await apiClient.request('/api/admin/secure');
console.log('User data:', userData);
} catch (error) {
if (error instanceof ApiError) {
if (error.isTokenExpiredError()) {
// Refresh token and retry
await refreshAuthToken();
return apiClient.request('/api/admin/secure');
} else if (error.isAuthenticationError()) {
// Redirect to login
redirectToLogin();
} else if (error.isAuthorizationError()) {
// Show access denied message
showAccessDeniedMessage();
}
}
}
Java Client (RestTemplate)ΒΆ
@Service
public class ApiClient {
private final RestTemplate restTemplate;
public ApiClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
// Configure error handling
restTemplate.setErrorHandler(new CustomResponseErrorHandler());
}
public <T> T makeRequest(String url, Class<T> responseType) {
try {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(getJwtToken());
HttpEntity<?> entity = new HttpEntity<>(headers);
ResponseEntity<T> response = restTemplate.exchange(
url, HttpMethod.GET, entity, responseType);
return response.getBody();
} catch (HttpClientErrorException ex) {
handleClientError(ex);
throw ex;
} catch (HttpServerErrorException ex) {
handleServerError(ex);
throw ex;
}
}
private void handleClientError(HttpClientErrorException ex) {
if (ex.getStatusCode() == HttpStatus.UNAUTHORIZED) {
// Handle authentication error
log.warn("Authentication failed: {}", ex.getResponseBodyAsString());
} else if (ex.getStatusCode() == HttpStatus.FORBIDDEN) {
// Handle authorization error
log.warn("Access denied: {}", ex.getResponseBodyAsString());
}
}
}
static class CustomResponseErrorHandler implements ResponseErrorHandler {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return response.getStatusCode().is4xxClientError() ||
response.getStatusCode().is5xxServerError();
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
String body = StreamUtils.copyToString(
response.getBody(), StandardCharsets.UTF_8);
ErrorResponse error = objectMapper.readValue(body, ErrorResponse.class);
if (response.getStatusCode().is4xxClientError()) {
throw new ApiClientException(error);
} else {
throw new ApiServerException(error);
}
}
}
π§ͺ Testing Error ScenariosΒΆ
Unit Testing Error ResponsesΒΆ
@Test
void shouldReturn401WhenNoAuthenticationProvided() throws Exception {
mockMvc.perform(get("/api/admin/secure"))
.andExpect(status().isUnauthorized())
.andExpected(jsonPath("$.status").value(401))
.andExpected(jsonPath("$.error").value("Unauthorized"))
.andExpect(jsonPath("$.message").value("Authentication required"))
.andExpect(jsonPath("$.path").value("/api/admin/secure"));
}
@Test
void shouldReturn403WhenInsufficientRole() throws Exception {
mockMvc.perform(get("/api/admin/secure")
.with(jwt().authorities(new SimpleGrantedAuthority("ROLE_USER"))))
.andExpect(status().isForbidden())
.andExpected(jsonPath("$.status").value(403))
.andExpect(jsonPath("$.error").value("Access Denied"));
}
@Test
void shouldReturn400WhenInvalidLoginParameters() throws Exception {
mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("username", "")
.param("password", ""))
.andExpect(status().isBadRequest())
.andExpected(jsonPath("$.status").value(400))
.andExpected(jsonPath("$.details.validationErrors").isArray());
}
Integration Testing with Error ScenariosΒΆ
@Test
void shouldHandleJwtTokenExpiry() {
// Create expired JWT token
String expiredToken = jwtUtil.generateExpiredToken("user", "ROLE_USER");
// Make request with expired token
ResponseEntity<Map> response = restTemplate.exchange(
"/api/user/secure",
HttpMethod.GET,
new HttpEntity<>(createAuthHeaders(expiredToken)),
Map.class
);
// Verify error response
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
assertThat(response.getBody().get("error")).isEqualTo("Unauthorized");
assertThat(response.getBody().get("details"))
.extracting("tokenStatus")
.isEqualTo("EXPIRED");
}
π Error Response Patterns SummaryΒΆ
Standard Error Response FormatΒΆ
{
"timestamp": "2024-01-15T10:30:00.123Z",
"status": 403,
"error": "Access Denied",
"message": "You don't have permission to access this resource",
"details": {
"additional": "context-specific information"
},
"path": "/api/admin/secure"
}
Error Response FieldsΒΆ
| Field | Type | Description | Required |
|---|---|---|---|
timestamp | ISO 8601 | When the error occurred | β |
status | Integer | HTTP status code | β |
error | String | Error category/type | β |
message | String | Human-readable description | β |
details | Object | Additional error context | β |
path | String | Request path that caused error | β |
π Next StepsΒΆ
- REST Endpoints β - Complete endpoint reference
- Authentication Flow β - Authentication sequence patterns
- Security Configuration β - Security implementation details
- Testing Examples β - Comprehensive testing patterns
π¨ Proper error handling improves API usability and debugging. Consistent error response formats help clients handle different error scenarios gracefully and provide better user experiences.