• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 22217583285

20 Feb 2026 08:51AM UTC coverage: 85.938% (+0.3%) from 85.68%
22217583285

Pull #9966

github

web-flow
Merge 3948c7063 into 5c78ba265
Pull Request #9966: refactor: cleanup `Exceptions`

7 of 32 new or added lines in 3 files covered. (21.88%)

1 existing line in 1 file now uncovered.

22264 of 25907 relevant lines covered (85.94%)

206.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.46
/system/Debug/ExceptionHandler.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Debug;
15

16
use Closure;
17
use CodeIgniter\API\ResponseTrait;
18
use CodeIgniter\Exceptions\PageNotFoundException;
19
use CodeIgniter\HTTP\CLIRequest;
20
use CodeIgniter\HTTP\Exceptions\HTTPException;
21
use CodeIgniter\HTTP\IncomingRequest;
22
use CodeIgniter\HTTP\RequestInterface;
23
use CodeIgniter\HTTP\ResponseInterface;
24
use Config\Paths;
25
use Throwable;
26

27
/**
28
 * @see \CodeIgniter\Debug\ExceptionHandlerTest
29
 */
30
final class ExceptionHandler extends BaseExceptionHandler implements ExceptionHandlerInterface
31
{
32
    use ResponseTrait;
33

34
    /**
35
     * ResponseTrait needs this.
36
     */
37
    private ?RequestInterface $request = null;
38

39
    /**
40
     * ResponseTrait needs this.
41
     */
42
    private ?ResponseInterface $response = null;
43

44
    /**
45
     * Determines the correct way to display the error.
46
     *
47
     * @param CLIRequest|IncomingRequest $request
48
     */
49
    public function handle(
50
        Throwable $exception,
51
        RequestInterface $request,
52
        ResponseInterface $response,
53
        int $statusCode,
54
        int $exitCode,
55
    ): void {
56
        // ResponseTrait needs these properties.
57
        $this->request  = $request;
3✔
58
        $this->response = $response;
3✔
59

60
        if ($request instanceof IncomingRequest) {
3✔
61
            try {
62
                $response->setStatusCode($statusCode);
2✔
63
            } catch (HTTPException) {
×
64
                // Workaround for invalid HTTP status code.
65
                $statusCode = 500;
×
66
                $response->setStatusCode($statusCode);
×
67
            }
68

69
            if (! headers_sent()) {
2✔
70
                header(
2✔
71
                    sprintf(
2✔
72
                        'HTTP/%s %s %s',
2✔
73
                        $request->getProtocolVersion(),
2✔
74
                        $response->getStatusCode(),
2✔
75
                        $response->getReasonPhrase(),
2✔
76
                    ),
2✔
77
                    true,
2✔
78
                    $statusCode,
2✔
79
                );
2✔
80
            }
81

82
            // Handles non-HTML requests.
83
            if (! str_contains($request->getHeaderLine('accept'), 'text/html')) {
2✔
84
                // If display_errors is enabled, shows the error details.
85
                $data = $this->isDisplayErrorsEnabled()
1✔
86
                    ? $this->collectVars($exception, $statusCode)
1✔
87
                    : '';
×
88

89
                // Sanitize data to remove non-JSON-serializable values (resources, closures)
90
                // before formatting for API responses (JSON, XML, etc.)
91
                if ($data !== '') {
1✔
92
                    $data = $this->sanitizeData($data);
1✔
93
                }
94

95
                $this->respond($data, $statusCode)->send();
1✔
96

97
                if (ENVIRONMENT !== 'testing') {
1✔
NEW
98
                    exit($exitCode); // @codeCoverageIgnore
×
99
                }
100

101
                return;
1✔
102
            }
103
        }
104

105
        // Determine possible directories of error views
106
        $addPath = ($request instanceof IncomingRequest ? 'html' : 'cli') . DIRECTORY_SEPARATOR;
2✔
107
        $path    = $this->viewPath . $addPath;
2✔
108
        $altPath = rtrim((new Paths())->viewDirectory, '\\/ ')
2✔
109
            . DIRECTORY_SEPARATOR . 'errors' . DIRECTORY_SEPARATOR . $addPath;
2✔
110

111
        // Determine the views
112
        $view    = $this->determineView($exception, $path, $statusCode);
2✔
113
        $altView = $this->determineView($exception, $altPath, $statusCode);
2✔
114

115
        // Check if the view exists
116
        $viewFile = null;
2✔
117
        if (is_file($path . $view)) {
2✔
118
            $viewFile = $path . $view;
2✔
119
        } elseif (is_file($altPath . $altView)) {
×
120
            $viewFile = $altPath . $altView;
×
121
        }
122

123
        // Displays the HTML or CLI error code.
124
        $this->render($exception, $statusCode, $viewFile);
2✔
125

126
        if (ENVIRONMENT !== 'testing') {
2✔
NEW
127
            exit($exitCode); // @codeCoverageIgnore
×
128
        }
129
    }
130

131
    /**
132
     * Determines the view to display based on the exception thrown, HTTP status
133
     * code, whether an HTTP or CLI request, etc.
134
     *
135
     * @return string The filename of the view file to use
136
     */
137
    protected function determineView(
138
        Throwable $exception,
139
        string $templatePath,
140
        int $statusCode = 500,
141
    ): string {
142
        // Production environments should have a custom exception file.
143
        $view = 'production.php';
6✔
144

145
        if ($this->isDisplayErrorsEnabled()) {
6✔
146
            // If display_errors is enabled, shows the error details.
147
            $view = 'error_exception.php';
5✔
148
        }
149

150
        // 404 Errors
151
        if ($exception instanceof PageNotFoundException) {
6✔
152
            return 'error_404.php';
3✔
153
        }
154

155
        $templatePath = rtrim($templatePath, '\\/ ') . DIRECTORY_SEPARATOR;
3✔
156

157
        // Allow for custom views based upon the status code
158
        if (is_file($templatePath . 'error_' . $statusCode . '.php')) {
3✔
159
            return 'error_' . $statusCode . '.php';
×
160
        }
161

162
        return $view;
3✔
163
    }
164

165
    private function isDisplayErrorsEnabled(): bool
166
    {
167
        return in_array(
7✔
168
            strtolower(ini_get('display_errors')),
7✔
169
            ['1', 'true', 'on', 'yes'],
7✔
170
            true,
7✔
171
        );
7✔
172
    }
173

174
    /**
175
     * Sanitizes data to remove non-JSON-serializable values like resources and closures.
176
     * This is necessary for API responses that need to be JSON/XML encoded.
177
     *
178
     * @param array<int, bool> $seen Used internally to prevent infinite recursion
179
     */
180
    private function sanitizeData(mixed $data, array &$seen = []): mixed
181
    {
182
        $type = gettype($data);
8✔
183

184
        switch ($type) {
185
            case 'resource':
8✔
186
            case 'resource (closed)':
7✔
187
                return '[Resource #' . (int) $data . ']';
4✔
188

189
            case 'array':
7✔
190
                $result = [];
2✔
191

192
                foreach ($data as $key => $value) {
2✔
193
                    $result[$key] = $this->sanitizeData($value, $seen);
2✔
194
                }
195

196
                return $result;
2✔
197

198
            case 'object':
7✔
199
                $oid = spl_object_id($data);
4✔
200
                if (isset($seen[$oid])) {
4✔
201
                    return '[' . $data::class . ' Object *RECURSION*]';
1✔
202
                }
203
                $seen[$oid] = true;
4✔
204

205
                if ($data instanceof Closure) {
4✔
206
                    return '[Closure]';
1✔
207
                }
208

209
                $result = [];
3✔
210

211
                foreach ((array) $data as $key => $value) {
3✔
212
                    $cleanKey          = preg_replace('/^\x00.*\x00/', '', (string) $key);
3✔
213
                    $result[$cleanKey] = $this->sanitizeData($value, $seen);
3✔
214
                }
215

216
                return $result;
3✔
217

218
            default:
219
                return $data;
5✔
220
        }
221
    }
222
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc