1. phases
Itinera API
  • ✈ Itinera API Documentation
  • Docs
    • legacy
      • Itinera — System Overview
      • Technology Stack & Architecture
      • Getting Started Guide
      • Infrastructure
      • Frontend Application
      • Architecture Overview
      • API Reference
      • Backend Services
      • Development Guidelines
    • phases
      • 01. Architecture Overview & Checkout Idempotency
      • Phase 2: Commerce & Checkout Engine
      • Phase 3: Webhooks & Asynchronous Fulfillment
      • Phase 4: Security Perimeter & Authentication
      • Phase 5: Database Schema & Entity Relationships
      • Phase 6: AI Quota & Telemetry Subsystems
      • Phase 7: API Route Matrix & FormRequests
      • Phase 8: Global Exception & Error Handling
      • Phase 9: Frontend Ecosystem & State Management
      • Phase 10: Design System & Component Library
      • Phase 11: Interactive GSAP Animations
      • Phase 12: Deployment & CI/CD Pipeline
      • Phase 13: Testing Strategies
      • Phase 14: Performance & Optimization
      • Phase 15: Developer Onboarding & Runbooks
      • 15-Phase Comprehensive Wiki & Documentation Plan
  • APIs
    • Auth
      • Register a new user
      • Log in a user
      • Log out user
      • Refresh JWT token
      • Forgot password request
      • Reset password verification
      • Get current user profile
      • Update user profile
      • Redirect to Google OAuth
      • Google OAuth Callback
      • Verify email via signed URL
    • Catalog
      • List all countries
      • Get country details
      • List all cities
      • List all regions
      • List all destinations
      • Get destination details
      • Get hotels by destination
      • List all hotels
      • Get hotel details
      • Get reviews for a hotel
      • List all flights
      • Get flight details
      • List all restaurants
      • Get restaurant details
      • List all attractions
      • Get current weather
      • Submit review for an entity
      • Delete review
      • Toggle favourite status for entity
      • List my submitted reviews
    • Bookings
      • Book a tour destination
    • V1 Aliases
      • V1 List all countries
      • V1 Get country details
      • V1 List all cities
      • V1 List all destinations
      • V1 Get destination details
      • V1 Get hotels by destination
      • V1 List all hotels
      • V1 Get hotel details
      • V1 Get reviews for a hotel
      • V1 List all flights
      • V1 Get flight details
      • V1 List all restaurants
      • V1 Get restaurant details
      • V1 List all attractions
      • V1 Get attraction details
      • V1 List all regions
      • V1 Get weather details
    • Trips
      • List user trips
      • Create a new trip
      • Get trip details
      • Update trip details
      • Delete a trip
      • Get creation metadata
      • Attach items to a trip
      • Update trip item
      • Detach items from a trip
      • Fork a trip
    • Conversations
      • List user conversations
      • Start a new conversation
      • Get conversation details
      • List messages in conversation
      • Send message to conversation
      • Mark conversation as read
    • Commerce Plans
      • List public plans
      • Get public plan details
    • Commerce Subscriptions
      • Subscribe to a plan
      • Upgrade active plan
      • Get active subscription info
      • Cancel active subscription
    • Commerce Checkout
      • Initiate Paymob payment checkout
    • Integrations
      • Paymob status webhook callback
      • Paymob redirect return callback
    • System Settings & Support
      • Submit public contact message
      • Subscribe to system newsletter
      • Get list of my reports
      • List all notifications
      • Mark single notification as read
      • List available surveys
      • Submit answers for survey
      • Get survey details
      • Update survey details
      • Delete survey response
    • AI Tools
      • Enhance itinerary details using AI
      • Request AI review of itinerary
      • Plan route using AI assistance
      • Get AI quota remaining details
      • Chat with AI Concierge assistant
      • Get AI Review progress by ID
    • Agency Integration
      • Request agency assignment
      • List agency active tasks
      • List agency managed trips
      • Get agency total earnings
      • Get agency profile details
      • Update agency profile details
    • Admin User Management
      • List users inside admin dashboard
      • Get user profile
      • Set user active status
      • Block user profile
    • Admin Catalog Moderation
      • Create new catalog category
      • Create new catalog destination
      • Create new hotel catalog record
      • Create new flight catalog record
      • Create new restaurant catalog record
      • Create new attraction catalog record
  • Schemas
    • User
    • ErrorResponse
    • Trip
    • Destination
    • Hotel
    • Flight
    • Restaurant
    • Attraction
    • Booking
    • Review
    • Agency
    • Survey
  1. phases

Phase 8: Global Exception & Error Handling

This document dissects the centralized, JSON-only error contract enforced in bootstrap/app.php and App\Exceptions\ApiExceptionHandler. Every failure — whether validation, auth, RBAC, routing, or database constraint — is normalized into a single envelope so frontend, mobile, and Postman consumers never branch on Accept headers or HTML error pages.

1. Pure API Mode: bootstrap/app.php#

The application is configured as a pure JSON API. Two guarantees are installed at bootstrap:
shouldRenderJsonWhen => true — Laravel will never render an HTML error page, even for curl without Accept: application/json. Every exception is forced to JSON.
Single renderable — All throwables flow through one class: ApiExceptionHandler::render(). No scattered reportable() closures, no per-exception render() overrides. One router, one shape.

2. Handler Map: Throwable → HTTP Status#

ApiExceptionHandler::$handlers is an ordered map. render() iterates top-to-bottom with instanceof checks; first match wins. Specific subclasses must be listed before their parents (ValidationException before HttpException).
ExceptionStatusType fieldUser Message
AuthenticationException401AuthenticationExceptionAuthentication required. Please provide valid credentials.
AuthorizationException, AccessDeniedHttpException, UnauthorizedException (Spatie)403AuthorizationException / UnauthorizedExceptionYou do not have permission to perform this action.
ValidationException422ValidationExceptionThe provided data is invalid. + validation_errors bag
ModelNotFoundException, NotFoundHttpException404ModelNotFoundException / NotFoundHttpExceptionThe requested resource was not found. / The requested endpoint '{uri}' was not found.
MethodNotAllowedHttpException405MethodNotAllowedHttpExceptionThe {METHOD} method is not allowed... + allowed_methods
HttpException (generic)$e->getStatusCode()HttpException$e->getMessage() ?: 'An HTTP error occurred.'
QueryException409 / 500QueryExceptionFK 1451 → Cannot delete... referenced ; 1062 → A record... already exists. ; else A database error occurred.
InvalidStateTransitionException409invalid_state_transitioncustom message (see §4)
unhandled500 or $e->getStatusCode()basename(get_class($e))An unexpected error occurred.

3. Envelope Contracts: App\Support\ApiResponse#

Two static factories produce the only JSON shapes the API ever returns.
Success:
{
  "success": true,
  "message": "Success",
  "data": { ... }
}
Built by ApiResponse::success($data, $message, $status, $extra).
Failure:
{
  "error": {
    "type": "ValidationException",
    "status": 422,
    "message": "The provided data is invalid.",
    "timestamp": "2026-08-26T12:00:00.000000Z",
    "validation_errors": [
      { "field": "email", "message": "The email field is required." }
    ]
  }
}
Built by ApiResponse::fail($message, $type, $status, $extras) where type = basename(get_class($e)).
Note: This is the ground truth. The error envelope is not {success:false, message, data} — it is {error:{type,status,message,timestamp,...}}. The success envelope is {success:true, message, data}. Consumers must branch on success vs error at the top level.

4. 422 Field Bags: handleValidationException#

Validation is enforced exclusively via FormRequest classes (Phase 7). When rules() fail, Laravel throws ValidationException before the controller executes. The handler flattens it:
Shape: validation_errors is a flat array of {field, message} — not the Laravel default {field:[messages]}. Easier for SPA form libraries to map.
Logging: Context includes the full bag: Log::warning('Validation failed', ['errors'=> $errors]).
Example — POST /api/checkout with bad payload:
HTTP/1.1 422 Unprocessable Entity
{
  "error": {
    "type": "ValidationException",
    "status": 422,
    "message": "The provided data is invalid.",
    "timestamp": "2026-08-26T10:00:00Z",
    "validation_errors": [
      { "field": "type", "message": "The selected type is invalid." },
      { "field": "idempotency_key", "message": "The idempotency key must not be greater than 64 characters." }
    ]
  }
}

5. DB & State Conflicts: 409 Semantics#

Two sources produce 409 Conflict:
QueryException with MySQL errorInfo[1] === 1062 (duplicate) or 1451 (FK restrict) → mapped to 409 with domain messages.
InvalidStateTransitionException extends AuthorizationException — thrown by domain services when an entity cannot transition (e.g., Order already FULFILLED). Its own render() emits {error:{type:"invalid_state_transition", status:409}} and is caught before the generic 403 handler because the handler map tests instanceof AuthorizationException — note: InvalidStateTransitionException is an AuthorizationException, so handler order matters; its dedicated render() is invoked via model binding, not the central map, giving 409 instead of 403.

6. Logging: logException#

Every handled branch logs via Log::warning with uniform context: exception, message, file, line, url, method, ip, plus branch extras (errors, sql, allowed_methods). Unhandled throwables also log as Unhandled exception before returning 500.

7. Flow Diagrams#

7.1 Central Router Flowchart#

7.2 Validation Field-Bag Sequence#

8. Architectural Principles#

1.
One envelope, one router. bootstrap/app.php installs a single renderable; ApiExceptionHandler owns the status→type→message table. No controller try/catch duplicates it.
2.
Never HTML. shouldRenderJsonWhen => true eliminates content-negotiation branches — QA and route:list consumers see JSON even for 404/405.
3.
Type = class basename. getExceptionType() uses basename(get_class($e)) (or explicit invalid_state_transition). Frontend can switch on error.type without parsing messages.
4.
Field bags are flat. {field, message}[] avoids field->[messages] nesting; frontend can groupBy(field) if needed.
5.
Constraints surface as 409, not 500. Duplicate key 1062 and FK 1451 are user errors, not server errors — mapped to 409 Conflict with actionable messages.
Modified at 2026-08-25 22:41:45
Previous
Phase 7: API Route Matrix & FormRequests
Next
Phase 9: Frontend Ecosystem & State Management
Built with