Exception-to-HTTP Mapping

Status: Complete | Last Updated: 2025-11-20 | Version: 1.0.0

Overview

This document provides a complete reference for how each exception type in Smart Supply Pro is mapped to HTTP status codes. It serves as both a decision tree for developers (when to throw which exception) and a reference guide for API consumers (what status codes to expect).


Quick Lookup Table

HTTP Status Exception Error Token Common Causes Handler
400 MethodArgumentNotValidException bad_request @Valid annotation failed on request body Global
400 ConstraintViolationException bad_request JSR-380 constraint failed on parameter Global
400 HttpMessageNotReadableException bad_request Malformed JSON in request body Global
400 MissingServletRequestParameterException bad_request Required query parameter missing Global
400 MethodArgumentTypeMismatchException bad_request Path/query param type conversion failed Global
400 InvalidRequestException bad_request Business validation failed (custom domain) Business
401 AuthenticationException unauthorized User not authenticated or credentials invalid Global
403 AccessDeniedException forbidden User lacks required permissions/role Global
404 NoSuchElementException not_found Resource lookup returned empty Global
404 IllegalArgumentException not_found Invalid argument caused lookup failure Global
409 DuplicateResourceException conflict Uniqueness constraint violation (custom) Business
409 DataIntegrityViolationException conflict Database constraint violation (FK, UNIQUE, CHECK) Global
409 ObjectOptimisticLockingFailureException conflict Concurrent update (version field mismatch) Global
409 IllegalStateException conflict Business state rule violation (custom) Business
500 Exception (unhandled) internal_server_error Any unhandled exception Global

Status Code 400: Bad Request

When to Use 400

Use 400 Bad Request when the request itself is malformed, invalid, or doesn’t meet requirements: - βœ… Client sent invalid/missing data - βœ… Request body JSON is malformed - βœ… Query parameters have wrong type - βœ… Required fields are missing - βœ… Business validation rules failed - ❌ Resource doesn’t exist (use 404) - ❌ User not authenticated (use 401) - ❌ User lacks permission (use 403)

Exceptions Mapped to 400

1. MethodArgumentNotValidException

Trigger: @Valid annotation validation fails on request body

@PostMapping("/suppliers")
public SupplierDTO create(@Valid @RequestBody CreateSupplierRequest req) {
    // If req violates @Valid constraints β†’ MethodArgumentNotValidException
}

Example Response:

{
  "error": "bad_request",
  "message": "Validation failed: name is required; email must be valid format; quantity must be greater than 0",
  "timestamp": "2025-11-20T14:30:45.123Z",
  "correlationId": "SSP-1700551445123-4891"
}

What Client Should Do:

if (error.status === 400 && error.data.error === 'bad_request') {
    // Extract field errors from message
    const messages = error.data.message.split(';');
    messages.forEach(msg => {
        const [field, description] = msg.trim().split(':');
        displayFieldError(field, description);
    });
}

2. ConstraintViolationException

Trigger: JSR-380 constraint validation fails on method parameters

@GetMapping("/{id}")
public SupplierDTO get(@NotNull @Min(1) @PathVariable Long id) {
    // If id is null or < 1 β†’ ConstraintViolationException
}

Example Response:

{
  "error": "bad_request",
  "message": "Constraint violation: id: must be greater than or equal to 1",
  "timestamp": "2025-11-20T14:30:46.234Z",
  "correlationId": "SSP-1700551446234-5902"
}

Common Annotations:

@NotNull           // Value must not be null
@NotEmpty          // String must not be empty
@NotBlank          // String must not be blank (spaces don't count)
@Min(n)            // Number must be >= n
@Max(n)            // Number must be <= n
@Pattern(regex)    // String must match regex
@Email             // Must be valid email format
@Size(min,max)     // Collection size must be between min and max

3. HttpMessageNotReadableException

Trigger: Request body JSON parsing fails (malformed JSON)

@PostMapping
public SupplierDTO create(@RequestBody CreateSupplierRequest req) {
    // POST /suppliers
    // Body: {"name": invalid json} β†’ HttpMessageNotReadableException
}

Example Response:

{
  "error": "bad_request",
  "message": "Invalid request format: malformed JSON or missing required fields",
  "timestamp": "2025-11-20T14:30:47.345Z",
  "correlationId": "SSP-1700551447345-6013"
}

Common Causes: - Missing quotes around string values - Trailing commas in JSON - Missing closing braces - Invalid characters - Non-numeric value for numeric field

Example Bad Request:

// Bad: Missing quotes, trailing comma
{"name": "ACME", "email": john@example.com,}

// Good
{"name": "ACME", "email": "john@example.com"}

4. MissingServletRequestParameterException

Trigger: Required query/form parameter is missing

@GetMapping
public List<SupplierDTO> search(
    @RequestParam(required = true) String category,
    @RequestParam(required = false) String region
) {
    // GET /suppliers (no category param) β†’ MissingServletRequestParameterException
}

Example Response:

{
  "error": "bad_request",
  "message": "Missing required parameter: category (String)",
  "timestamp": "2025-11-20T14:30:48.456Z",
  "correlationId": "SSP-1700551448456-7124"
}

Correct Request:

GET /suppliers?category=FOOD

5. MethodArgumentTypeMismatchException

Trigger: Path/query parameter type conversion fails

@GetMapping("/{id}")
public SupplierDTO get(@PathVariable Long id) {
    // GET /suppliers/abc (abc is not a number) β†’ MethodArgumentTypeMismatchException
}

Example Response:

{
  "error": "bad_request",
  "message": "Invalid Long for parameter 'id': received 'abc'",
  "timestamp": "2025-11-20T14:30:49.567Z",
  "correlationId": "SSP-1700551449567-8235"
}

Valid Request:

GET /suppliers/123

6. InvalidRequestException (Domain)

Trigger: Custom domain validation failure thrown explicitly

// In service layer
if (request.getQuantity() < 1) {
    throw InvalidRequestException.valueOutOfRange("quantity", 1, 999999);
}

Example Response:

{
  "error": "bad_request",
  "message": "Validation failed: quantity value must be between 1 and 999999",
  "timestamp": "2025-11-20T14:30:50.678Z",
  "correlationId": "SSP-1700551450678-9346"
}

Factory Methods:

InvalidRequestException.requiredField("email")
InvalidRequestException.invalidFormat("sku", "XXX-YYYY-ZZZZ")
InvalidRequestException.valueOutOfRange("quantity", 1, 999)
InvalidRequestException.businessRuleViolation("Cannot order zero quantity")

Status Code 401: Unauthorized

When to Use 401

Use 401 Unauthorized when authentication is missing or invalid: - βœ… User hasn’t logged in (no token) - βœ… Token is expired - βœ… Token is invalid (tampered with) - βœ… Credentials (username/password) don’t match - ❌ User is authenticated but lacks permission (use 403)

Exceptions Mapped to 401

AuthenticationException

Trigger: Authentication fails (credentials invalid, no token, expired token)

@GetMapping("/profile")
@PreAuthorize("isAuthenticated()")
public ProfileDTO getProfile() {
    // If user not authenticated β†’ AuthenticationException
}

Example Response:

{
  "error": "unauthorized",
  "message": "Authentication failed: invalid credentials or expired session",
  "timestamp": "2025-11-20T14:30:51.789Z",
  "correlationId": "SSP-1700551451789-0457"
}

Security Note: Message is generic to prevent user enumeration (doesn’t reveal if username exists or password is wrong).

Frontend Handling:

if (error.status === 401) {
    // Redirect to login page
    window.location.href = '/login';
    
    // Optional: show login-required message
    showMessage('Your session has expired. Please log in again.');
}

Status Code 403: Forbidden

When to Use 403

Use 403 Forbidden when user is authenticated but lacks permission for the operation: - βœ… User doesn’t have required role (ADMIN, MANAGER, etc.) - βœ… User lacks specific permission - βœ… User is not the owner of the resource - βœ… Operation is disabled for this user’s account status - ❌ User isn’t logged in (use 401) - ❌ Resource doesn’t exist (use 404)

Exceptions Mapped to 403

AccessDeniedException

Trigger: User lacks required permissions/role

@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public void deleteSupplier(@PathVariable Long id) {
    // If user is not ADMIN β†’ AccessDeniedException
}

Example Response:

{
  "error": "forbidden",
  "message": "Access denied: insufficient permissions for this operation",
  "timestamp": "2025-11-20T14:30:52.890Z",
  "correlationId": "SSP-1700551452890-1568"
}

Security Note: Message is generic (doesn’t reveal which role/permission is required).

Frontend Handling:

if (error.status === 403) {
    // Show "unauthorized" message
    showMessage('You do not have permission to perform this action');
    
    // Disable the UI element that triggered the request
    disableButton();
    
    // Optional: show "contact admin" message
    showContactAdminMessage();
}

Status Code 404: Not Found

When to Use 404

Use 404 Not Found when resource doesn’t exist: - βœ… User requests a supplier that doesn’t exist - βœ… Resource was deleted - βœ… Wrong resource ID provided - ❌ Parameter is wrong type (use 400) - ❌ Required field is missing (use 400) - ❌ User not authenticated (use 401)

Exceptions Mapped to 404

NoSuchElementException

Trigger: Resource lookup returns empty (Optional.empty())

@GetMapping("/{id}")
public SupplierDTO get(@PathVariable Long id) {
    return supplierRepository.findById(id)
            .orElseThrow(() -> new NoSuchElementException("Supplier not found"));
}

Example Response:

{
  "error": "not_found",
  "message": "Supplier not found",
  "timestamp": "2025-11-20T14:30:53.901Z",
  "correlationId": "SSP-1700551453901-2679"
}

IllegalArgumentException

Trigger: Invalid argument in lookup operation

@GetMapping("/by-status/{status}")
public List<SupplierDTO> getByStatus(@PathVariable String status) {
    SupplierStatus enumStatus = SupplierStatus.valueOf(status);
    // If status = "UNKNOWN" β†’ IllegalArgumentException
    return supplierRepository.findByStatus(enumStatus);
}

Example Response:

{
  "error": "not_found",
  "message": "No enum constant com.smartsupplypro.SupplierStatus.UNKNOWN",
  "timestamp": "2025-11-20T14:30:54.012Z",
  "correlationId": "SSP-1700551454012-3780"
}

Status Code 409: Conflict

When to Use 409

Use 409 Conflict when request conflicts with current state: - βœ… Resource already exists (duplicate unique field) - βœ… Concurrent update conflict (someone else modified resource) - βœ… Business state rule violation (e.g., can’t delete supplier with active items) - ❌ Resource doesn’t exist (use 404) - ❌ User not authenticated (use 401) - ❌ User lacks permission (use 403)

Exceptions Mapped to 409

DuplicateResourceException (Domain)

Trigger: Uniqueness constraint violation (custom)

@PostMapping
public SupplierDTO create(CreateSupplierRequest req) {
    if (supplierRepository.existsByName(req.getName())) {
        throw DuplicateResourceException.supplierName(req.getName());
    }
    return supplierRepository.save(supplier);
}

Example Response:

{
  "error": "conflict",
  "message": "Supplier with name 'ACME Inc' already exists",
  "timestamp": "2025-11-20T14:30:55.123Z",
  "correlationId": "SSP-1700551455123-4891"
}

With Field Error Details:

// Parse context from exception
{
  "error": "conflict",
  "message": "Supplier with name 'ACME Inc' already exists",
  "resourceType": "Supplier",
  "conflictField": "name",
  "duplicateValue": "ACME Inc"
}

DataIntegrityViolationException

Trigger: Database constraint violation (UNIQUE, FOREIGN KEY, CHECK)

@PostMapping
public SupplierDTO create(CreateSupplierRequest req) {
    return supplierRepository.save(supplier);
    // If database has UNIQUE constraint on email β†’ DataIntegrityViolationException
}

Example Response:

{
  "error": "conflict",
  "message": "Resource with this value already exists",
  "timestamp": "2025-11-20T14:30:56.234Z",
  "correlationId": "SSP-1700551456234-5902"
}

Sanitization: - ❌ Raw error: β€œUNIQUE constraint failed: SUPPLIERS.EMAIL” - βœ… Sanitized: β€œResource with this value already exists”


ObjectOptimisticLockingFailureException

Trigger: Concurrent update conflict (optimistic locking version mismatch)

@PutMapping("/{id}")
public SupplierDTO update(@PathVariable Long id, @RequestBody UpdateRequest req) {
    Supplier supplier = supplierRepository.findById(id).orElseThrow();
    supplier.setName(req.getName());
    supplier.setVersion(req.getVersion()); // Version must match current DB version
    return supplierRepository.save(supplier);
    // If another user modified supplier since fetch β†’ ObjectOptimisticLockingFailureException
}

Example Response:

{
  "error": "conflict",
  "message": "Resource was modified by another user. Please refresh and try again.",
  "timestamp": "2025-11-20T14:30:57.345Z",
  "correlationId": "SSP-1700551457345-6013"
}

Frontend Handling:

try {
    await api.put(`/suppliers/${id}`, { ...data, version: 5 });
} catch (error) {
    if (error.status === 409 && error.data.error === 'conflict') {
        // Refresh from server and retry
        const latest = await api.get(`/suppliers/${id}`);
        showRetryDialog(latest);
    }
}

IllegalStateException (Domain)

Trigger: Business state rule violation

@DeleteMapping("/{id}")
public void deleteSupplier(@PathVariable Long id) {
    Supplier supplier = supplierRepository.findById(id).orElseThrow();
    
    if (supplier.getStatus() == DELETED) {
        throw new IllegalStateException("Cannot modify already-deleted supplier");
    }
    
    if (inventoryRepository.existsBySupplier(supplier)) {
        throw new IllegalStateException("Cannot delete supplier with active inventory");
    }
}

Example Response:

{
  "error": "conflict",
  "message": "Cannot delete supplier with active inventory",
  "timestamp": "2025-11-20T14:30:58.456Z",
  "correlationId": "SSP-1700551458456-7124"
}

Status Code 500: Internal Server Error

When to Use 500

Use 500 Internal Server Error for unhandled exceptions that don’t fit other categories: - βœ… NullPointerException - βœ… Database connection timeout - βœ… File system access error - βœ… Third-party API failure - βœ… Any exception not explicitly handled - ❌ Use 400-409 for client/request errors - ❌ Use 401/403 for auth errors

Exceptions Mapped to 500

Generic Exception (Catch-All)

Trigger: Any unhandled exception

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpected(Exception ex) {
    logger.error("Unhandled exception", ex); // Full stack trace logged
    
    return ErrorResponse.builder()
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .message("An unexpected error occurred. Please contact support.")
            .build();
}

Example Response:

{
  "error": "internal_server_error",
  "message": "An unexpected error occurred. Please contact support.",
  "timestamp": "2025-11-20T14:30:59.567Z",
  "correlationId": "SSP-1700551459567-8235"
}

Security Note: No stack trace, no exception details exposed to client (prevent information disclosure).

Frontend Handling:

if (error.status === 500) {
    const correlationId = error.data.correlationId;
    
    showErrorMessage(`Error: ${error.data.message}`);
    showSupportContact(`Please report error ID: ${correlationId}`);
    
    // Log for telemetry/analytics
    analytics.error({
        status: error.status,
        correlationId: correlationId,
        timestamp: error.data.timestamp,
        url: window.location.href
    });
}

Decision Tree

Use this flowchart to determine the appropriate HTTP status code:

graph TD A["Exception Thrown"] --> B{{"Exception Type?"}} B -->|Request Format Invalid| C{{"Which Kind?"}} B -->|Authentication Missing| D["401
Unauthorized
AuthenticationException"] B -->|Insufficient Permission| E["403
Forbidden
AccessDeniedException"] B -->|Resource Not Found| F{{"Which Kind?"}} B -->|State Conflict| G{{"Which Kind?"}} B -->|Unhandled| H["500
Internal Server Error"] C -->|Malformed JSON| C1["400
Bad Request
HttpMessageNotReadableException"] C -->|Missing Parameter| C2["400
Bad Request
MissingServletRequestParameterException"] C -->|Type Mismatch| C3["400
Bad Request
MethodArgumentTypeMismatchException"] C -->|Validation Failed| C4["400
Bad Request
MethodArgumentNotValidException"] C -->|Business Rule Violated| C5["400
Bad Request
InvalidRequestException"] F -->|Not Found in Optional| F1["404
Not Found
NoSuchElementException"] F -->|Invalid Enum Value| F2["404
Not Found
IllegalArgumentException"] G -->|Duplicate Key| G1["409
Conflict
DuplicateResourceException"] G -->|DB Constraint Violated| G2["409
Conflict
DataIntegrityViolationException"] G -->|Concurrent Update| G3["409
Conflict
ObjectOptimisticLockingFailureException"] G -->|Business State Violated| G4["409
Conflict
IllegalStateException"]

Frontend Error Handling Pattern

Implement this pattern to handle all status codes consistently:

async function makeRequest(method, url, data = null) {
    try {
        const response = await fetch(url, {
            method,
            headers: { 'Content-Type': 'application/json' },
            body: data ? JSON.stringify(data) : null
        });
        
        if (!response.ok) {
            const error = await response.json();
            handleHttpError(response.status, error);
            return null;
        }
        
        return await response.json();
    } catch (error) {
        handleNetworkError(error);
        return null;
    }
}

function handleHttpError(status, errorData) {
    const { error, message, correlationId } = errorData;
    
    switch (status) {
        case 400:
            // User error - show validation message
            showValidationError(message);
            break;
        case 401:
            // Authentication error - redirect to login
            redirectToLogin();
            break;
        case 403:
            // Permission error - show access denied
            showAccessDenied();
            break;
        case 404:
            // Not found - show resource doesn't exist
            showNotFound(message);
            break;
        case 409:
            // Conflict - handle retry or conflict resolution
            if (error === 'conflict') {
                showConflictDialog(message);
            }
            break;
        case 500:
            // Server error - show generic message with correlation ID
            showServerError(correlationId);
            break;
        default:
            showUnknownError();
    }
}


Version History

Version Date Changes
1.0.0 2025-11-20 Initial comprehensive mapping documentation

Quick Reference Card

Print this for quick reference:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Status  β”‚ Error Token            β”‚ When to Use              β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 400     β”‚ bad_request            β”‚ Invalid request data     β”‚
β”‚ 401     β”‚ unauthorized           β”‚ Not authenticated        β”‚
β”‚ 403     β”‚ forbidden              β”‚ Insufficient permission  β”‚
β”‚ 404     β”‚ not_found              β”‚ Resource doesn't exist   β”‚
β”‚ 409     β”‚ conflict               β”‚ State/duplicate conflict β”‚
β”‚ 500     β”‚ internal_server_error  β”‚ Unhandled exception      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
HOW TO DETERMINE STATUS CODE:

1. Is request malformed?              β†’ 400
2. Is user not authenticated?         β†’ 401
3. Does user lack permission?         β†’ 403
4. Does resource not exist?           β†’ 404
5. Does state/duplicate conflict?     β†’ 409
6. Is it an unhandled error?          β†’ 500