Skip to content

Exceptions

Jean-Marc Strauven edited this page Dec 23, 2025 · 1 revision

Exceptions

Learn about the exceptions thrown by Laravel ApiRoute and how to handle them.


Overview

Laravel ApiRoute throws specific exceptions for API version errors:

Exception When Thrown
VersionNotFoundException Requested version doesn't exist
VersionSunsetException Version has reached end-of-life
InvalidVersionException Version format is invalid
ApiRouteException Base exception class

VersionNotFoundException

Thrown when the requested API version doesn't exist.

When Thrown

// Request to non-existent version
GET /api/v99/users  → VersionNotFoundException

Exception Class

<?php

namespace Grazulex\ApiRoute\Exceptions;

class VersionNotFoundException extends ApiRouteException
{
    public function __construct(
        public readonly string $requestedVersion
    ) {
        parent::__construct("API version '{$requestedVersion}' not found.");
    }

    public function render(Request $request): JsonResponse
    {
        return response()->json([
            'error' => 'version_not_found',
            'message' => $this->getMessage(),
            'requested_version' => $this->requestedVersion,
            'available_versions' => ApiRoute::versions()->pluck('name'),
        ], 404);
    }
}

Response

{
    "error": "version_not_found",
    "message": "API version 'v99' not found.",
    "requested_version": "v99",
    "available_versions": ["v1", "v2", "v3"]
}

HTTP Status: 404 Not Found


VersionSunsetException

Thrown when a sunset version is accessed and rejection is configured.

When Thrown

// Version with past sunset date + 'reject' action
GET /api/v1/users  → VersionSunsetException (if v1 is sunset)

Configuration

// config/apiroute.php
'sunset' => [
    'action' => 'reject',  // Only throws when 'reject'
    'status_code' => 410,
],

Exception Class

<?php

namespace Grazulex\ApiRoute\Exceptions;

class VersionSunsetException extends ApiRouteException
{
    public function __construct(
        public readonly VersionDefinition $version
    ) {
        parent::__construct("API version '{$version->name()}' has been sunset.");
    }

    public function render(Request $request): JsonResponse
    {
        $config = config('apiroute');
        $migrationGuides = $config['documentation']['migration_guides'] ?? [];

        return response()->json([
            'error' => 'api_version_sunset',
            'message' => "API version {$this->version->name()} is no longer available.",
            'sunset_date' => $this->version->sunsetDate()?->toIso8601String(),
            'successor' => $this->version->successor(),
            'migration_guide' => $migrationGuides[$this->version->name()] ?? null,
        ], $config['sunset']['status_code'] ?? 410);
    }
}

Response

{
    "error": "api_version_sunset",
    "message": "API version v1 is no longer available.",
    "sunset_date": "2024-12-01T00:00:00+00:00",
    "successor": "v2",
    "migration_guide": "https://docs.example.com/migration/v1-to-v2"
}

HTTP Status: 410 Gone (configurable)


InvalidVersionException

Thrown when the version format is invalid.

When Thrown

// Invalid version format
X-API-Version: invalid  → InvalidVersionException

Response

{
    "error": "invalid_version",
    "message": "Invalid API version format: 'invalid'"
}

HTTP Status: 400 Bad Request


ApiRouteException

Base exception class for all package exceptions.

<?php

namespace Grazulex\ApiRoute\Exceptions;

use Exception;

class ApiRouteException extends Exception
{
    // Base class for all ApiRoute exceptions
}

Catching All Package Exceptions

try {
    // API logic
} catch (ApiRouteException $e) {
    // Handle any ApiRoute exception
}

Custom Exception Handling

Global Exception Handler

In Laravel 11+ (bootstrap/app.php):

use Grazulex\ApiRoute\Exceptions\VersionNotFoundException;
use Grazulex\ApiRoute\Exceptions\VersionSunsetException;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (VersionNotFoundException $e, Request $request) {
        return response()->json([
            'error' => 'version_not_found',
            'message' => $e->getMessage(),
            'help' => 'Visit /api/versions for available versions',
        ], 404);
    });

    $exceptions->render(function (VersionSunsetException $e, Request $request) {
        // Custom sunset response
        return response()->json([
            'error' => 'api_version_sunset',
            'message' => 'Please upgrade to a newer API version',
            'upgrade_url' => 'https://docs.example.com/upgrade',
        ], 410);
    });
})

Legacy Exception Handler

In Laravel 10 (app/Exceptions/Handler.php):

<?php

namespace App\Exceptions;

use Grazulex\ApiRoute\Exceptions\VersionNotFoundException;
use Grazulex\ApiRoute\Exceptions\VersionSunsetException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;

class Handler extends ExceptionHandler
{
    public function register(): void
    {
        $this->renderable(function (VersionNotFoundException $e, $request) {
            return response()->json([
                'error' => 'version_not_found',
                'message' => $e->getMessage(),
            ], 404);
        });

        $this->renderable(function (VersionSunsetException $e, $request) {
            return response()->json([
                'error' => 'version_sunset',
                'message' => 'This API version is no longer available.',
            ], 410);
        });
    }
}

Error Response Formats

Consistent Error Format

Create a custom error response format:

<?php

namespace App\Http\Responses;

class ApiErrorResponse
{
    public static function make(
        string $error,
        string $message,
        int $status,
        array $extra = []
    ): JsonResponse {
        return response()->json([
            'success' => false,
            'error' => [
                'code' => $error,
                'message' => $message,
                'status' => $status,
                ...$extra,
            ],
            'timestamp' => now()->toIso8601String(),
        ], $status);
    }
}

Using Custom Format

$exceptions->render(function (VersionSunsetException $e, Request $request) {
    return ApiErrorResponse::make(
        error: 'api_version_sunset',
        message: $e->getMessage(),
        status: 410,
        extra: [
            'successor' => $e->version->successor(),
            'migration_guide' => config("apiroute.documentation.migration_guides.{$e->version->name()}"),
        ]
    );
});

Exception Status Codes

Configurable Status Codes

// config/apiroute.php
'sunset' => [
    'status_code' => 410,  // HTTP Gone (default)
],

Alternative Status Codes

Code Meaning When to Use
410 Gone Resource permanently removed (default) Standard sunset
404 Not Found Resource doesn't exist Legacy clients
426 Upgrade Required Client must upgrade Force migration

Logging Exceptions

Log All Version Errors

$exceptions->render(function (ApiRouteException $e, Request $request) {
    Log::warning('API version error', [
        'error' => get_class($e),
        'message' => $e->getMessage(),
        'path' => $request->path(),
        'ip' => $request->ip(),
        'user_agent' => $request->userAgent(),
    ]);

    // Let default rendering handle response
    return null;
});

Testing Exceptions

Test Version Not Found

test('returns 404 for non-existent version', function () {
    $response = $this->getJson('/api/v99/users');

    $response->assertStatus(404);
    $response->assertJson([
        'error' => 'version_not_found',
    ]);
});

Test Version Sunset

test('returns 410 for sunset version', function () {
    // Setup sunset version
    ApiRoute::version('v1', fn() => Route::get('users', fn() => 'v1'))
        ->sunset(now()->subDay());

    $response = $this->getJson('/api/v1/users');

    $response->assertStatus(410);
    $response->assertJson([
        'error' => 'api_version_sunset',
    ]);
});

Test Exception Properties

test('sunset exception contains version info', function () {
    $version = new VersionDefinition('v1', fn() => null);
    $version->sunset(now()->subDay());
    $version->setSuccessor('v2');

    $exception = new VersionSunsetException($version);

    expect($exception->version->name())->toBe('v1');
    expect($exception->version->successor())->toBe('v2');
});

Preventing Exceptions

Check Before Request

use Grazulex\ApiRoute\Facades\ApiRoute;

// In API gateway or middleware
if (!ApiRoute::hasVersion($requestedVersion)) {
    return redirect()->route('api.versions');
}

if (ApiRoute::isSunset($requestedVersion)) {
    return redirect()->route('api.upgrade', ['from' => $requestedVersion]);
}

Next Steps

Clone this wiki locally