Skip to content

Latest commit

 

History

History
564 lines (441 loc) · 14 KB

File metadata and controls

564 lines (441 loc) · 14 KB
sidebar_position 8

Exception Handling

PHP Swagger Test uses exceptions to communicate validation failures and errors. Understanding the exception hierarchy and knowing when each exception is thrown will help you debug issues and handle errors appropriately.

Exception Hierarchy

All exceptions extend from BaseException, which provides additional context through the getBody() method:

classDiagram
    Exception <|-- BaseException
    BaseException <|-- DefinitionNotFoundException
    BaseException <|-- PathNotFoundException
    BaseException <|-- HttpMethodNotFoundException
    BaseException <|-- InvalidRequestException
    BaseException <|-- InvalidDefinitionException
    BaseException <|-- NotMatchedException
    BaseException <|-- StatusCodeNotMatchedException
    BaseException <|-- RequiredArgumentNotFound
    BaseException <|-- GenericApiException

    class BaseException {
        +getBody() mixed
    }
Loading

BaseException

The base class for all library exceptions.

Special Feature: Provides a getBody() method that returns the request/response body that caused the error.

<?php
use ByJG\ApiTools\Exception\BaseException;

try {
    $this->sendRequest($request);
} catch (BaseException $e) {
    // Get the actual body that failed validation
    $failedBody = $e->getBody();

    echo $e->getMessage();  // Human-readable error
    print_r($failedBody);   // The data that caused the error
}

Request/Path Exceptions

PathNotFoundException

Thrown when: The requested path doesn't exist in the OpenAPI specification.

Common causes:

  • Path not defined in the spec
  • Missing leading slash
  • Base path included incorrectly

Example:

// OpenAPI spec has: /pet/{petId}
// But you're testing: /pets/1  (wrong path)

$request = new ApiRequester();
$request->withMethod('GET')->withPath('/pets/1');  // ❌ Wrong

try {
    $this->sendRequest($request);
} catch (\ByJG\ApiTools\Exception\PathNotFoundException $e) {
    // Path "/pets/1" not found
    echo $e->getMessage();
}

// Fix: Use correct path
$request->withPath('/pet/1');  // ✅ Correct

HttpMethodNotFoundException

Thrown when: The HTTP method doesn't exist for a given path in the specification.

Common causes:

  • Wrong HTTP method (GET instead of POST)
  • Method not defined in the spec for that path

Example:

// OpenAPI spec has: POST /pet but you're using GET

$request = new ApiRequester();
$request->withMethod('GET')->withPath('/pet');  // ❌ Wrong method

try {
    $this->sendRequest($request);
} catch (\ByJG\ApiTools\Exception\HttpMethodNotFoundException $e) {
    // The http method 'get' not found in '/pet'
    echo $e->getMessage();
}

// Fix: Use correct method
$request->withMethod('POST')->withPath('/pet');  // ✅ Correct

Definition Exceptions

DefinitionNotFoundException

Thrown when: A referenced schema definition (via $ref) cannot be found.

Common causes:

  • Invalid $ref path
  • Missing definition in definitions (Swagger 2.0) or components/schemas (OpenAPI 3.0)
  • Typo in definition name

Example:

# OpenAPI spec has a broken reference
components:
  schemas:
    Pet:
      type: object
      properties:
        category:
          $ref: '#/components/schemas/Categoryy'  # Typo: should be 'Category'
try {
    $this->sendRequest($request);
} catch (\ByJG\ApiTools\Exception\DefinitionNotFoundException $e) {
    // Not find reference #/components/schemas/Categoryy
    echo $e->getMessage();
}

Fix: Correct the reference in your OpenAPI specification.

InvalidDefinitionException

Thrown when: The OpenAPI specification has structural problems or invalid definitions.

Common causes:

  • Malformed $ref paths
  • Invalid schema structure
  • Missing required schema properties

Example:

# Invalid $ref format
components:
  parameters:
    limitParam:
      $ref: 'components/parameters/limit'  # ❌ Wrong format
try {
    $this->sendRequest($request);
} catch (\ByJG\ApiTools\Exception\InvalidDefinitionException $e) {
    // Not get the reference in the expected format #/components/parameters/<NAME>
    echo $e->getMessage();
}

Fix: Use proper $ref format: #/components/parameters/limit


Request/Response Validation Exceptions

NotMatchedException

Thrown when: Request or response data doesn't match the OpenAPI specification.

Common causes:

  • Wrong data type (string instead of integer)
  • Value doesn't match pattern/enum
  • Unexpected properties (when additionalProperties: false)
  • Missing properties in response
  • Value doesn't match constraints (min/max, minLength/maxLength)

Examples:

Type Mismatch

// Spec expects integer, you send string
$request = new ApiRequester();
$request
    ->withMethod('POST')
    ->withPath('/pet')
    ->withRequestBody([
        'id' => 'abc',  // ❌ Should be integer
        'name' => 'Fluffy'
    ]);

try {
    $this->sendRequest($request);
} catch (\ByJG\ApiTools\Exception\NotMatchedException $e) {
    // Value 'abc' in 'id' is not integer
    echo $e->getMessage();
    print_r($e->getBody());  // Shows the full request body
}

Pattern Mismatch

// Spec has pattern: '^[a-z]+$' (only lowercase)
$request = new ApiRequester();
$request
    ->withMethod('POST')
    ->withPath('/pet')
    ->withRequestBody([
        'name' => 'Fluffy123'  // ❌ Contains numbers
    ]);

try {
    $this->sendRequest($request);
} catch (\ByJG\ApiTools\Exception\NotMatchedException $e) {
    // Value 'Fluffy123' in 'name' not matched in pattern
    echo $e->getMessage();
}

Enum Mismatch

// Spec has enum: ['available', 'pending', 'sold']
$request = new ApiRequester();
$request
    ->withMethod('POST')
    ->withPath('/pet')
    ->withRequestBody([
        'status' => 'unknown'  // ❌ Not in enum
    ]);

try {
    $this->sendRequest($request);
} catch (\ByJG\ApiTools\Exception\NotMatchedException $e) {
    // Value 'unknown' in 'status' not matched in ENUM
    echo $e->getMessage();
}

Additional Properties

// Spec has additionalProperties: false
$request = new ApiRequester();
$request
    ->withMethod('POST')
    ->withPath('/pet')
    ->withRequestBody([
        'name' => 'Fluffy',
        'extraField' => 'value'  // ❌ Not defined in spec
    ]);

try {
    $this->sendRequest($request);
} catch (\ByJG\ApiTools\Exception\NotMatchedException $e) {
    // Additional properties are not allowed
    echo $e->getMessage();
}

Header Not Found

$request = new ApiRequester();
$request
    ->withMethod('GET')
    ->withPath('/pet/1')
    ->expectHeaderContains('X-Custom-Header', 'value');

try {
    $this->sendRequest($request);
} catch (\ByJG\ApiTools\Exception\NotMatchedException $e) {
    // Does not exists header 'X-Custom-Header' with value 'value'
    echo $e->getMessage();
}

Body Content Missing

$request = new ApiRequester();
$request
    ->withMethod('GET')
    ->withPath('/pet/1')
    ->expectBodyContains('Fluffy');

try {
    $this->sendRequest($request);
} catch (\ByJG\ApiTools\Exception\NotMatchedException $e) {
    // Body does not contain 'Fluffy'
    echo $e->getMessage();
}

RequiredArgumentNotFound

Thrown when: A required property is missing from the request or response.

Example:

// Spec requires 'name' and 'photoUrls'
$request = new ApiRequester();
$request
    ->withMethod('POST')
    ->withPath('/pet')
    ->withRequestBody([
        'name' => 'Fluffy'
        // ❌ Missing 'photoUrls'
    ]);

try {
    $this->sendRequest($request);
} catch (\ByJG\ApiTools\Exception\RequiredArgumentNotFound $e) {
    // The property 'photoUrls' is required but not found
    echo $e->getMessage();
}

StatusCodeNotMatchedException

Thrown when: The actual HTTP status code doesn't match the expected status.

Example:

$request = new ApiRequester();
$request
    ->withMethod('GET')
    ->withPath('/pet/999999')
    ->expectStatus(200);  // Expecting success

try {
    $this->sendRequest($request);
} catch (\ByJG\ApiTools\Exception\StatusCodeNotMatchedException $e) {
    // Status code not matched: Expected 200, got 404
    echo $e->getMessage();
    print_r($e->getBody());  // Shows the error response body
}

InvalidRequestException

Thrown when: The request is malformed or invalid.

Common causes:

  • Empty path
  • Invalid Content-Type
  • Malformed request structure
  • Invalid path parameters

Example:

$request = new ApiRequester();
$request
    ->withMethod('POST')
    ->withPath('/pet')
    ->withRequestBody('invalid data')
    ->withRequestHeader(['Content-Type' => 'application/xml']);  // ❌ Unsupported

try {
    $this->sendRequest($request);
} catch (\ByJG\ApiTools\Exception\InvalidRequestException $e) {
    // Cannot handle Content Type 'application/xml'
    echo $e->getMessage();
}

Generic Exceptions

GenericApiException

Thrown when: A general API validation error occurs that doesn't fit other categories.

This is a catch-all exception for unexpected validation errors.

try {
    $this->sendRequest($request);
} catch (\ByJG\ApiTools\Exception\GenericApiException $e) {
    echo $e->getMessage();
}

Best Practices for Exception Handling

1. Catch Specific Exceptions First

use ByJG\ApiTools\Exception\PathNotFoundException;
use ByJG\ApiTools\Exception\StatusCodeNotMatchedException;
use ByJG\ApiTools\Exception\NotMatchedException;
use ByJG\ApiTools\Exception\BaseException;

try {
    $this->sendRequest($request);
} catch (PathNotFoundException $e) {
    // Handle missing path - maybe your spec is outdated
    $this->markTestSkipped("Path not in spec: " . $e->getMessage());
} catch (StatusCodeNotMatchedException $e) {
    // Handle unexpected status code
    echo "Expected different status. Response: " . json_encode($e->getBody());
    throw $e;
} catch (NotMatchedException $e) {
    // Handle validation failures
    echo "Validation failed: " . $e->getMessage();
    echo "Body: " . json_encode($e->getBody());
    throw $e;
} catch (BaseException $e) {
    // Catch all other library exceptions
    throw $e;
}

2. Use getBody() for Debugging

The getBody() method on BaseException provides the actual data that caused the error:

try {
    $this->sendRequest($request);
} catch (\ByJG\ApiTools\Exception\NotMatchedException $e) {
    // Log the error with full context
    error_log("Validation Error: " . $e->getMessage());
    error_log("Failed Body: " . json_encode($e->getBody(), JSON_PRETTY_PRINT));

    // Or save to file for analysis
    file_put_contents(
        'failed-request.json',
        json_encode($e->getBody(), JSON_PRETTY_PRINT)
    );
}

3. Custom Error Handling in Tests

class MyApiTest extends ApiTestCase
{
    protected function handleValidationError(\ByJG\ApiTools\Exception\BaseException $e): void
    {
        // Custom logging
        $this->logger->error($e->getMessage(), [
            'body' => $e->getBody(),
            'trace' => $e->getTraceAsString()
        ]);

        // Take screenshot for UI tests
        if ($this->isUiTest) {
            $this->takeScreenshot('validation-error-' . time());
        }
    }

    public function testApiEndpoint(): void
    {
        try {
            $request = new ApiRequester();
            $request->withMethod('GET')->withPath('/pet/1');
            $this->sendRequest($request);
        } catch (\ByJG\ApiTools\Exception\BaseException $e) {
            $this->handleValidationError($e);
            throw $e;
        }
    }
}

4. Graceful Degradation in Production

When using runtime validation in production, handle exceptions gracefully:

use ByJG\ApiTools\Base\Schema;

class ApiController
{
    private Schema $schema;

    public function handleRequest($requestData): Response
    {
        try {
            // Validate request against OpenAPI spec
            $bodyRequestDef = $this->schema->getRequestParameters('/api/users', 'POST');
            $bodyRequestDef->match($requestData);

            // Process valid request
            $result = $this->processRequest($requestData);

            // Validate response
            $bodyResponseDef = $this->schema->getResponseParameters('/api/users', 'POST', 200);
            $bodyResponseDef->match($result);

            return new Response($result, 200);

        } catch (\ByJG\ApiTools\Exception\RequiredArgumentNotFound $e) {
            return new Response([
                'error' => 'Missing required field',
                'message' => $e->getMessage()
            ], 400);

        } catch (\ByJG\ApiTools\Exception\NotMatchedException $e) {
            return new Response([
                'error' => 'Invalid request data',
                'message' => $e->getMessage()
            ], 400);

        } catch (\ByJG\ApiTools\Exception\BaseException $e) {
            // Log unexpected errors
            error_log("API Validation Error: " . $e->getMessage());

            return new Response([
                'error' => 'Request validation failed'
            ], 400);
        }
    }
}

Exception Quick Reference

Exception When Thrown HTTP Status Hint
PathNotFoundException Path not in spec 404 (likely)
HttpMethodNotFoundException Method not allowed for path 405 (likely)
DefinitionNotFoundException Schema reference not found 500 (spec error)
InvalidDefinitionException Spec structure invalid 500 (spec error)
NotMatchedException Data doesn't match schema 400 (validation)
RequiredArgumentNotFound Missing required field 400 (validation)
StatusCodeNotMatchedException Wrong status code N/A
InvalidRequestException Malformed request 400 (client error)
GenericApiException General API error Varies

Related Documentation