Skip to content

Conversation

@Snider
Copy link
Contributor

@Snider Snider commented Feb 2, 2026

This submission introduces a custom exception hierarchy and a global exception handler to improve the application's error handling and recovery mechanisms, and it includes an audit report that details the findings and the changes that were made.

Fixes #51


PR created automatically by Jules for task 13970562873216773264 started by @Snider

Summary by CodeRabbit

  • New Features

    • Standardized API error responses now return JSON with appropriate HTTP status codes (403 for authorization errors, 422 for validation errors, 400 for others).
  • Bug Fixes

    • Improved error handling for authorization failures with clearer error messages.
  • Chores

    • Updated CI/CD pipeline for static analysis output handling.

✏️ Tip: You can customize this high-level summary in your review settings.

…dler

This commit introduces a custom exception hierarchy and a global exception handler to improve the application's error handling and recovery mechanisms.

Key changes include:
- A new `ApiException` class and more specific exception types (`AuthorizationException`, `ValidationException`).
- A new global exception handler in `src/Core/Exception/Handler.php` that catches `ApiException` types and returns standardized JSON error responses.
- The new exception handler is registered in `src/Core/Boot.php`.
- The `ManagesTokens` trait has been refactored to use the new `AuthorizationException`.
- An `AUDIT-ERRORS.md` file has been created to document the findings of an error-handling audit.

Co-authored-by: Snider <631881+Snider@users.noreply.github.com>
@google-labs-jules
Copy link

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@coderabbitai
Copy link

coderabbitai bot commented Feb 2, 2026

📝 Walkthrough

Walkthrough

The changes introduce a structured error-handling framework with custom exception hierarchy (ApiException as base, with AuthorizationException and ValidationException subclasses), a global exception handler that returns standardized JSON responses with appropriate HTTP status codes, and update existing code to use these new exception types. A composer manifest is added, and the QA workflow is modified to exclude stderr from Psalm output files.

Changes

Cohort / File(s) Summary
Exception Framework
src/Core/Exception/ApiException.php, src/Core/Exception/AuthorizationException.php, src/Core/Exception/ValidationException.php, src/Core/Exception/Handler.php
Introduces custom exception hierarchy extending built-in Exception class; Handler implements JSON response rendering with status code mapping (403 for AuthorizationException, 422 for ValidationException, 400 for others) and excludes ApiException from logging.
Integration & Setup
src/Core/LifecycleEventProvider.php, original_composer.json, AUDIT-ERRORS.md
Registers custom exception handler in service container; adds new composer manifest with project metadata, dependencies, and scripts; documents error-handling audit findings.
Code Refactoring
src/Plug/Concern/ManagesTokens.php
Updates token validation to throw AuthorizationException instead of generic Exception, with corresponding docblock updates.
Workflow
.github/workflows/qa.yml
Removes stderr redirection from Psalm commands; diagnostic output no longer captured in artifact files.

Sequence Diagram

sequenceDiagram
    participant Client
    participant App as Application
    participant Handler as Exception Handler
    participant Renderer as Renderable
    participant Response

    Client->>App: Send Request
    App->>App: Process Request
    alt Token Not Configured
        App->>App: throw AuthorizationException
    else Validation Failed
        App->>App: throw ValidationException
    else Other Error
        App->>App: throw ApiException
    end
    
    App->>Handler: Caught Exception
    Handler->>Handler: Determine HTTP Status Code
    Handler->>Renderer: Match Exception Type
    
    alt AuthorizationException
        Renderer->>Renderer: HTTP 403
    else ValidationException
        Renderer->>Renderer: HTTP 422
    else ApiException
        Renderer->>Renderer: HTTP 400
    end
    
    Renderer->>Response: Build JSON Response
    Response->>Client: Return JSON with Status Code
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

Poem

🐰 Oh what a framework we've built with such care,
With exceptions that hop from here to there!
Authorization and validation so bright,
Handler returns JSON responses just right,
Errors now graceful—no more generic plight! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Out of Scope Changes check ❓ Inconclusive Changes in .github/workflows/qa.yml (Psalm output redirection fix) are supporting infrastructure fixes to resolve CI issues, not core audit deliverables; original_composer.json appears to be a reference/backup file rather than a deliverable change. Clarify whether the QA workflow changes and original_composer.json are intentional inclusions or accidental artifacts that should be removed from this audit-focused PR.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[Audit] Error Handling and Recovery' directly aligns with the main objective of implementing a custom exception hierarchy and global exception handler for improved error handling.
Linked Issues check ✅ Passed The PR fulfills all coding requirements from issue #51: implements exception hierarchy consistency (ApiException base with AuthorizationException, ValidationException), establishes error boundaries (global handler), standardizes user-facing errors (JSON responses), and documents findings in AUDIT-ERRORS.md.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/error-handling-audit-13970562873216773264

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Feb 2, 2026

QA Pipeline Results

Check Status Details
Tests 207 tests, 410 assertions
PHPStan 0 errors
Psalm 1 issues
Code Style 3 files need formatting
Security 0 vulnerabilities
Artifacts
  • test-results.xml - JUnit test results
  • phpstan.json - PHPStan analysis
  • psalm.json / psalm.sarif - Psalm analysis
  • pint.json - Code style report
  • audit.json - Security audit

Generated by core php qa pipeline

google-labs-jules bot and others added 3 commits February 2, 2026 01:28
This commit fixes a CI failure in the Psalm job of the QA workflow. The `2>&1` redirection was causing progress messages from `stderr` to be included in the JSON and SARIF output files, which corrupted them and caused the SARIF parser to fail.

By removing the `2>&1` redirection, the output files will now contain only the valid JSON and SARIF data, which should resolve the CI failure.

Co-authored-by: Snider <631881+Snider@users.noreply.github.com>
This commit fixes an `InvalidDocblock` error in `src/Core/Exception/Handler.php` that was causing the Psalm CI job to fail. The `@return void` tag has been removed from the `register` method's docblock, and the return type has been added to the function signature to adhere to modern PHP standards.

This change should resolve the Psalm error and allow the CI pipeline to pass.
This commit fixes a CI failure by correctly registering the custom exception handler. The previous method of registering the handler in `src/Core/Boot.php` was incorrect for Laravel 11 and caused a Psalm error that resulted in an invalid SARIF file.

The exception handler is now registered as a singleton in the `register` method of the `LifecycleEventProvider`, which is the correct approach for this version of Laravel. This should resolve the Psalm error and allow the CI pipeline to pass.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@original_composer.json`:
- Around line 1-86: The file original_composer.json is a redundant duplicate of
composer.json; remove original_composer.json from the repository to avoid
duplicate composer manifests and confusion, ensuring only composer.json remains
and updating any references (scripts, CI configs, or README) that might point to
original_composer.json to reference composer.json instead.

In `@src/Core/Exception/Handler.php`:
- Around line 39-51: The ApiException renderable currently returns only
['message' => ...]; enhance ValidationException to accept and expose a
field-level errors array (add a constructor, a private array property and a
getErrors(): array method) so it can carry per-field messages, then update the
renderable callback in Handler (the $this->renderable(function (ApiException $e,
Request $request) { ... }) block) to detect $e instanceof ValidationException
and include 'errors' => $e->getErrors() in the JSON response alongside
'message'; also review the dontReport list (which currently suppresses
ApiException subclasses) and either remove AuthorizationException from
dontReport or add a specific report() override for AuthorizationException to
ensure auth failures are logged if desired; lastly confirm that returning null
for non-JSON requests remains intentional so Laravel falls back to HTML
rendering.
🧹 Nitpick comments (5)
AUDIT-ERRORS.md (1)

23-23: Use UK English spelling.

Per coding guidelines, UK English spelling should be used. "Standardize" should be "standardise".

📝 Suggested fix
--   **Recommendation:** Standardize the creation of user-facing error messages, and ensure they are always clear, concise, and helpful.
+-   **Recommendation:** Standardise the creation of user-facing error messages, and ensure they are always clear, concise, and helpful.
src/Core/Exception/ApiException.php (1)

7-9: Consider adding a class docblock.

A brief docblock would clarify the purpose of this base exception class within the API exception hierarchy.

📝 Suggested enhancement
+/**
+ * Base exception for all API-related errors.
+ *
+ * Subclasses include AuthorizationException (403) and ValidationException (422).
+ * Handled by Core\Exception\Handler to produce standardised JSON responses.
+ */
 class ApiException extends \Exception
 {
 }
src/Core/Exception/ValidationException.php (1)

7-9: Consider adding validation error support.

Laravel's Illuminate\Validation\ValidationException includes structured error data via an errors() method and $validator property. This custom ValidationException is empty, so validation errors would only be in the message string.

If you intend to return structured validation errors (common for API 422 responses), consider adding properties to hold field-level errors:

♻️ Proposed enhancement
+/**
+ * Exception for validation failures in API requests.
+ *
+ * Rendered as HTTP 422 with JSON error response.
+ */
 class ValidationException extends ApiException
 {
+    /**
+     * `@param` array<string, array<string>> $errors Field-level validation errors
+     */
+    public function __construct(
+        string $message = 'The given data was invalid.',
+        protected array $errors = [],
+    ) {
+        parent::__construct($message);
+    }
+
+    /**
+     * `@return` array<string, array<string>>
+     */
+    public function errors(): array
+    {
+        return $this->errors;
+    }
 }

This would also require updating Handler::register() to include errors in the JSON response for ValidationException.

src/Core/Exception/Handler.php (2)

19-21: Verify that suppressing AuthorizationException logs aligns with security requirements.

Adding ApiException::class to $dontReport prevents all subclasses—including AuthorizationException—from being logged. While suppressing validation noise is reasonable, authorization failures (e.g., missing or invalid tokens) are often security-relevant events worth auditing. Consider whether AuthorizationException should be excluded from $dontReport or logged via a separate channel.


54-56: Consider removing the empty reportable callback if unused.

The no-op reportable callback adds no behaviour. If it's a placeholder for future logging integration, a brief comment noting the intent would help. Otherwise, it can be safely removed.

Comment on lines +1 to +86
{
"name": "host-uk/core",
"description": "Modular monolith framework for Laravel - event-driven architecture with lazy module loading",
"keywords": ["laravel", "modular", "monolith", "framework", "events", "modules"],
"license": "EUPL-1.2",
"authors": [
{
"name": "Host UK",
"email": "support@host.uk.com"
}
],
"require": {
"php": "^8.2",
"laravel/framework": "^11.0|^12.0",
"laravel/pennant": "^1.0",
"livewire/livewire": "^3.0|^4.0"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"infection/infection": "^0.32.3",
"larastan/larastan": "^3.9",
"laravel/pint": "^1.18",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"orchestra/testbench": "^9.0|^10.0",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpunit/phpunit": "^11.5",
"psalm/plugin-laravel": "^3.0",
"rector/rector": "^2.3",
"roave/security-advisories": "dev-latest",
"spatie/laravel-activitylog": "^4.8",
"vimeo/psalm": "^6.14"
},
"suggest": {
"spatie/laravel-activitylog": "Required for activity logging features (^4.0)"
},
"autoload": {
"psr-4": {
"Core\\": "src/Core/",
"Core\\Website\\": "src/Website/",
"Core\\Mod\\": "src/Mod/",
"Core\\Plug\\": "src/Plug/"
},
"files": [
"src/Core/Media/Thumbnail/helpers.php"
]
},
"autoload-dev": {
"psr-4": {
"Core\\Tests\\": "tests/",
"Core\\TestCore\\": "tests/Fixtures/Core/TestCore/",
"App\\Custom\\": "tests/Fixtures/Custom/",
"Mod\\": "tests/Fixtures/Mod/",
"Plug\\": "tests/Fixtures/Plug/",
"Website\\": "tests/Fixtures/Website/"
}
},
"scripts": {
"test": "vendor/bin/phpunit",
"pint": "vendor/bin/pint",
"stan": "vendor/bin/phpstan analyse --memory-limit=512M"
},
"extra": {
"laravel": {
"providers": [
"Core\\LifecycleEventProvider",
"Core\\Lang\\LangServiceProvider",
"Core\\Bouncer\\Gate\\Boot"
]
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"php-http/discovery": true,
"phpstan/extension-installer": true,
"infection/extension-installer": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if a composer.json already exists and compare with original_composer.json
if [ -f "composer.json" ]; then
    echo "=== composer.json exists ==="
    echo "Differences between composer.json and original_composer.json:"
    diff composer.json original_composer.json || echo "Files differ"
else
    echo "No composer.json found - original_composer.json may need to be renamed"
fi

# Also check for any other composer-related files
echo ""
echo "=== Searching for composer-related files ==="
find . -maxdepth 2 -name "*composer*" -type f 2>/dev/null | head -20

Repository: host-uk/core-php

Length of output: 237


🏁 Script executed:

# Check git history and file context
git log --oneline --all -- "*composer*" 2>/dev/null | head -20

Repository: host-uk/core-php

Length of output: 1345


🏁 Script executed:

# Look for documentation or comments about this file
git show HEAD:original_composer.json 2>/dev/null | head -10
# Also check if there's a .gitignore or setup instructions
find . -maxdepth 2 \( -name "README*" -o -name "SETUP*" -o -name ".gitignore" -o -name "docker-compose*" \) -type f 2>/dev/null

Repository: host-uk/core-php

Length of output: 471


🏁 Script executed:

# Check README for any mention of composer files or setup instructions
cat README.md | grep -i "composer\|original\|setup" -A 3 -B 3 || echo "No matches in README"

# Also check if there's any documentation in the root about this
ls -la | grep -i "doc\|read\|guide\|setup"

Repository: host-uk/core-php

Length of output: 459


🏁 Script executed:

# Check the .gitignore to see if original_composer.json should be ignored
cat .gitignore | head -30

Repository: host-uk/core-php

Length of output: 483


Remove the redundant original_composer.json file.

This file is identical to composer.json and serves no documented purpose. Having two identical composer manifests is unnecessary duplication and can cause confusion during maintenance. Delete original_composer.json and keep only composer.json.

🤖 Prompt for AI Agents
In `@original_composer.json` around lines 1 - 86, The file original_composer.json
is a redundant duplicate of composer.json; remove original_composer.json from
the repository to avoid duplicate composer manifests and confusion, ensuring
only composer.json remains and updating any references (scripts, CI configs, or
README) that might point to original_composer.json to reference composer.json
instead.

Comment on lines +39 to +51
$this->renderable(function (ApiException $e, Request $request) {
if ($request->wantsJson()) {
$statusCode = match (true) {
$e instanceof AuthorizationException => 403,
$e instanceof ValidationException => 422,
default => 400,
};

return new JsonResponse(
['message' => $e->getMessage()],
$statusCode
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/Core/Exception/Handler.php

Repository: host-uk/core-php

Length of output: 1889


🌐 Web query:

Laravel 11 exception handler renderable callback return null behavior

💡 Result:

In Laravel 11, an exception “render” / renderable callback is optional: if it returns null (or returns nothing), Laravel treats that as “not handled” and falls back to the framework’s normal exception rendering for that request (i.e., the default HTML/JSON error response). To override rendering, your callback must return a Response / JsonResponse / etc. [1]

Source: Laravel 11 “Error Handling → Rendering Exceptions” explicitly says: “If the closure given to the render method does not return a value, Laravel's default exception rendering will be utilized.” [1]

References

[1] Laravel 11.x Docs – Error Handling (Rendering Exceptions) https://laravel.com/docs/11.x/errors


🏁 Script executed:

fd -t f "ValidationException" src/ --exec cat -n {} \;

Repository: host-uk/core-php

Length of output: 233


🏁 Script executed:

rg -A 10 "class ValidationException" src/

Repository: host-uk/core-php

Length of output: 239


🏁 Script executed:

rg -B 5 -A 15 "ValidationException" src/Core/Exception/

Repository: host-uk/core-php

Length of output: 1769


🏁 Script executed:

rg -B 3 -A 3 "throw.*ValidationException" src/ --max-count=5

Repository: host-uk/core-php

Length of output: 42


🏁 Script executed:

rg -B 3 -A 3 "throw.*AuthorizationException" src/ --max-count=5

Repository: host-uk/core-php

Length of output: 799


🏁 Script executed:

rg "new ValidationException" src/ --max-count=5

Repository: host-uk/core-php

Length of output: 42


🏁 Script executed:

rg -A 20 "class ApiException" src/Core/Exception/

Repository: host-uk/core-php

Length of output: 209


🏁 Script executed:

rg "ApiException\|AuthorizationException" src/ -l | head -10

Repository: host-uk/core-php

Length of output: 42


🏁 Script executed:

rg "class.*Exception.*extends" src/Core/Exception/ -A 2

Repository: host-uk/core-php

Length of output: 587


Consider enhancing ValidationException to carry field-level validation errors if form validation is the intended use case.

The current response returns ['message' => $e->getMessage()] only. For form/field validation, clients typically expect ['message' => '...', 'errors' => [...]] to display per-field messages. The ValidationException class itself is currently empty with no error structure—if this exception should handle field-level validation failures, it needs to be enhanced to store and expose validation errors.

When $request->wantsJson() returns false, the callback returns null, which causes Laravel to use its default exception rendering (HTML error page). This is correct per Laravel documentation, but confirm this behavior is intentional for API exceptions thrown during non-JSON requests.

Additionally, dontReport suppresses all ApiException subclasses from logging, including AuthorizationException failures. Consider whether authorization failures should be logged separately for security audit purposes.

🤖 Prompt for AI Agents
In `@src/Core/Exception/Handler.php` around lines 39 - 51, The ApiException
renderable currently returns only ['message' => ...]; enhance
ValidationException to accept and expose a field-level errors array (add a
constructor, a private array property and a getErrors(): array method) so it can
carry per-field messages, then update the renderable callback in Handler (the
$this->renderable(function (ApiException $e, Request $request) { ... }) block)
to detect $e instanceof ValidationException and include 'errors' =>
$e->getErrors() in the JSON response alongside 'message'; also review the
dontReport list (which currently suppresses ApiException subclasses) and either
remove AuthorizationException from dontReport or add a specific report()
override for AuthorizationException to ensure auth failures are logged if
desired; lastly confirm that returning null for non-JSON requests remains
intentional so Laravel falls back to HTML rendering.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Audit] Error Handling and Recovery

2 participants