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

codeigniter4 / CodeIgniter4 / 37986190232

09 Oct 2026 08:19PM UTC coverage: 50.703% (-39.5%) from 90.183%
37986190232

Pull #10606

github

web-flow
Merge 2cebb1efa into 7822cde17
Pull Request #10606: fix: refresh REQUEST after URI GET normalization

17 of 30 new or added lines in 2 files covered. (56.67%)

10012 existing lines in 288 files now uncovered.

12871 of 25385 relevant lines covered (50.7%)

87.05 hits per line

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

0.0
/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.
UNCOV
57
        $this->request  = $request;
×
UNCOV
58
        $this->response = $response;
×
59

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

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

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

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

UNCOV
95
                $this->respond($data, $statusCode)->send();
×
96

UNCOV
97
                if (ENVIRONMENT !== 'testing') {
×
98
                    // @codeCoverageIgnoreStart
99
                    exit($exitCode);
100
                    // @codeCoverageIgnoreEnd
101
                }
102

UNCOV
103
                return;
×
104
            }
105
        }
106

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

113
        // Determine the views
UNCOV
114
        $view    = $this->determineView($exception, $path, $statusCode);
×
UNCOV
115
        $altView = $this->determineView($exception, $altPath, $statusCode);
×
116

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

125
        // Displays the HTML or CLI error code.
UNCOV
126
        $this->render($exception, $statusCode, $viewFile);
×
127

UNCOV
128
        if (ENVIRONMENT !== 'testing') {
×
129
            // @codeCoverageIgnoreStart
130
            exit($exitCode);
131
            // @codeCoverageIgnoreEnd
132
        }
133
    }
134

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

UNCOV
149
        if ($this->isDisplayErrorsEnabled()) {
×
150
            // If display_errors is enabled, shows the error details.
UNCOV
151
            $view = 'error_exception.php';
×
152
        }
153

154
        // 404 Errors
UNCOV
155
        if ($exception instanceof PageNotFoundException) {
×
UNCOV
156
            return 'error_404.php';
×
157
        }
158

UNCOV
159
        $templatePath = rtrim($templatePath, '\\/ ') . DIRECTORY_SEPARATOR;
×
160

161
        // Allow for custom views based upon the status code
UNCOV
162
        if (is_file($templatePath . 'error_' . $statusCode . '.php')) {
×
163
            return 'error_' . $statusCode . '.php';
×
164
        }
165

UNCOV
166
        return $view;
×
167
    }
168

169
    private function isDisplayErrorsEnabled(): bool
170
    {
UNCOV
171
        return in_array(
×
UNCOV
172
            strtolower(ini_get('display_errors')),
×
UNCOV
173
            ['1', 'true', 'on', 'yes'],
×
UNCOV
174
            true,
×
UNCOV
175
        );
×
176
    }
177

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

188
        switch ($type) {
UNCOV
189
            case 'resource':
×
UNCOV
190
            case 'resource (closed)':
×
UNCOV
191
                return '[Resource #' . (int) $data . ']';
×
192

UNCOV
193
            case 'array':
×
UNCOV
194
                $result = [];
×
195

UNCOV
196
                foreach ($data as $key => $value) {
×
UNCOV
197
                    $result[$key] = $this->sanitizeData($value, $seen);
×
198
                }
199

UNCOV
200
                return $result;
×
201

UNCOV
202
            case 'object':
×
UNCOV
203
                $oid = spl_object_id($data);
×
UNCOV
204
                if (isset($seen[$oid])) {
×
UNCOV
205
                    return '[' . $data::class . ' Object *RECURSION*]';
×
206
                }
UNCOV
207
                $seen[$oid] = true;
×
208

UNCOV
209
                if ($data instanceof Closure) {
×
UNCOV
210
                    return '[Closure]';
×
211
                }
212

UNCOV
213
                $result = [];
×
214

UNCOV
215
                foreach ((array) $data as $key => $value) {
×
UNCOV
216
                    $cleanKey          = preg_replace('/^\x00.*\x00/', '', (string) $key);
×
UNCOV
217
                    $result[$cleanKey] = $this->sanitizeData($value, $seen);
×
218
                }
219

UNCOV
220
                return $result;
×
221

222
            default:
UNCOV
223
                return $data;
×
224
        }
225
    }
226
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc