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

codeigniter4 / CodeIgniter4 / 28685001618

03 Jul 2026 09:55PM UTC coverage: 88.703% (-0.001%) from 88.704%
28685001618

Pull #10372

github

web-flow
Merge 857504f28 into 5e7c6efbd
Pull Request #10372: fix: resolve global state pollution causing random-order test failures in HTTP suite

11 of 12 new or added lines in 3 files covered. (91.67%)

3 existing lines in 2 files now uncovered.

22275 of 25112 relevant lines covered (88.7%)

213.42 hits per line

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

98.55
/system/HTTP/MessageTrait.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\HTTP;
15

16
use CodeIgniter\Exceptions\InvalidArgumentException;
17
use CodeIgniter\HTTP\Exceptions\HTTPException;
18

19
/**
20
 * Message Trait
21
 * Additional methods to make a PSR-7 Message class
22
 * compliant with the framework's own MessageInterface.
23
 *
24
 * @see https://github.com/php-fig/http-message/blob/master/src/MessageInterface.php
25
 */
26
trait MessageTrait
27
{
28
    /**
29
     * List of all HTTP request headers.
30
     *
31
     * [name => Header]
32
     * or
33
     * [name => [Header1, Header2]]
34
     *
35
     * @var array<string, Header|list<Header>>
36
     */
37
    protected $headers = [];
38

39
    /**
40
     * Holds a map of lower-case header names
41
     * and their normal-case key as it is in $headers.
42
     * Used for case-insensitive header access.
43
     *
44
     * @var array
45
     */
46
    protected $headerMap = [];
47

48
    // --------------------------------------------------------------------
49
    // Body
50
    // --------------------------------------------------------------------
51

52
    /**
53
     * Sets the body of the current message.
54
     *
55
     * @param string $data
56
     *
57
     * @return $this
58
     */
59
    public function setBody($data): self
60
    {
61
        $this->body = $data;
456✔
62

63
        return $this;
456✔
64
    }
65

66
    /**
67
     * Appends data to the body of the current message.
68
     *
69
     * @param string $data
70
     *
71
     * @return $this
72
     */
73
    public function appendBody($data): self
74
    {
75
        $this->body .= (string) $data;
10✔
76

77
        return $this;
10✔
78
    }
79

80
    // --------------------------------------------------------------------
81
    // Headers
82
    // --------------------------------------------------------------------
83

84
    /**
85
     * Populates the $headers array with any headers the server knows about.
86
     */
87
    public function populateHeaders(): void
88
    {
89
        $contentType = service('superglobals')->server('CONTENT_TYPE');
1,651✔
90
        if (! empty($contentType)) {
1,651✔
91
            $this->setHeader('Content-Type', $contentType);
8✔
92
        }
93
        unset($contentType);
1,651✔
94

95
        $serverArray = service('superglobals')->getServerArray();
1,651✔
96

97
        foreach (array_keys($serverArray) as $key) {
1,651✔
98
            if (sscanf($key, 'HTTP_%s', $header) === 1) {
1,568✔
99
                // take SOME_HEADER and turn it into Some-Header
100
                $header = str_replace('_', ' ', strtolower($header));
316✔
101
                $header = str_replace(' ', '-', ucwords($header));
316✔
102

103
                $this->setHeader($header, $serverArray[$key]);
316✔
104

105
                // Add us to the header map, so we can find them case-insensitively
106
                $this->headerMap[strtolower($header)] = $header;
316✔
107
            }
108
        }
109
    }
110

111
    /**
112
     * Returns an array containing all Headers.
113
     *
114
     * @return array<string, Header|list<Header>> An array of the Header objects
115
     */
116
    public function headers(): array
117
    {
118
        // If no headers are defined, but the user is
119
        // requesting it, then it's likely they want
120
        // it to be populated so do that...
121
        if (empty($this->headers)) {
84✔
122
            $this->populateHeaders();
4✔
123
        }
124

125
        return $this->headers;
84✔
126
    }
127

128
    /**
129
     * Returns a single Header object. If multiple headers with the same
130
     * name exist, then will return an array of header objects.
131
     *
132
     * @param string $name
133
     *
134
     * @return Header|list<Header>|null
135
     */
136
    public function header($name)
137
    {
138
        $origName = $this->getHeaderName($name);
128✔
139

140
        return $this->headers[$origName] ?? null;
128✔
141
    }
142

143
    /**
144
     * Sets a header and it's value.
145
     *
146
     * @param array|string|null $value
147
     *
148
     * @return $this
149
     */
150
    public function setHeader(string $name, $value): self
151
    {
152
        $this->checkMultipleHeaders($name);
1,246✔
153

154
        $origName = $this->getHeaderName($name);
1,246✔
155

156
        if (
157
            isset($this->headers[$origName])
1,246✔
158
            && is_array($this->headers[$origName]->getValue())
1,246✔
159
        ) {
160
            if (! is_array($value)) {
7✔
161
                $value = [$value];
4✔
162
            }
163

164
            foreach ($value as $v) {
7✔
165
                $this->appendHeader($origName, $v);
7✔
166
            }
167
        } else {
168
            $this->headers[$origName]               = new Header($origName, $value);
1,246✔
169
            $this->headerMap[strtolower($origName)] = $origName;
1,246✔
170
        }
171

172
        return $this;
1,246✔
173
    }
174

175
    private function hasMultipleHeaders(string $name): bool
176
    {
177
        $origName = $this->getHeaderName($name);
1,870✔
178

179
        return isset($this->headers[$origName]) && is_array($this->headers[$origName]);
1,870✔
180
    }
181

182
    private function checkMultipleHeaders(string $name): void
183
    {
184
        if ($this->hasMultipleHeaders($name)) {
1,246✔
185
            throw new InvalidArgumentException(
1✔
186
                'The header "' . $name . '" already has multiple headers.'
1✔
187
                . ' You cannot change them. If you really need to change, remove the header first.',
1✔
188
            );
1✔
189
        }
190
    }
191

192
    /**
193
     * Removes a header from the list of headers we track.
194
     *
195
     * @return $this
196
     */
197
    public function removeHeader(string $name): self
198
    {
199
        $origName = $this->getHeaderName($name);
880✔
200
        unset($this->headers[$origName], $this->headerMap[strtolower($name)]);
880✔
201

202
        return $this;
880✔
203
    }
204

205
    /**
206
     * Adds an additional header value to any headers that accept
207
     * multiple values (i.e. are an array or implement ArrayAccess)
208
     *
209
     * @return $this
210
     */
211
    public function appendHeader(string $name, ?string $value): self
212
    {
213
        $this->checkMultipleHeaders($name);
99✔
214

215
        $origName = $this->getHeaderName($name);
98✔
216

217
        array_key_exists($origName, $this->headers)
98✔
218
            ? $this->headers[$origName]->appendValue($value)
65✔
219
            : $this->setHeader($name, $value);
33✔
220

221
        return $this;
98✔
222
    }
223

224
    /**
225
     * Adds a header (not a header value) with the same name.
226
     * Use this only when you set multiple headers with the same name,
227
     * typically, for `Set-Cookie`.
228
     *
229
     * @return $this
230
     */
231
    public function addHeader(string $name, string $value): static
232
    {
233
        $origName = $this->getHeaderName($name);
31✔
234

235
        if (! isset($this->headers[$origName])) {
31✔
236
            $this->setHeader($name, $value);
30✔
237

238
            return $this;
30✔
239
        }
240

241
        if (! $this->hasMultipleHeaders($name) && isset($this->headers[$origName])) {
18✔
242
            $this->headers[$origName] = [$this->headers[$origName]];
18✔
243
        }
244

245
        // Add the header.
246
        $this->headers[$origName][] = new Header($origName, $value);
18✔
247

248
        return $this;
18✔
249
    }
250

251
    /**
252
     * Adds an additional header value to any headers that accept
253
     * multiple values (i.e. are an array or implement ArrayAccess)
254
     *
255
     * @return $this
256
     */
257
    public function prependHeader(string $name, string $value): self
258
    {
259
        $this->checkMultipleHeaders($name);
1✔
260

261
        $origName = $this->getHeaderName($name);
1✔
262

263
        $this->headers[$origName]->prependValue($value);
1✔
264

265
        return $this;
1✔
266
    }
267

268
    /**
269
     * Takes a header name in any case, and returns the
270
     * normal-case version of the header.
271
     */
272
    protected function getHeaderName(string $name): string
273
    {
274
        return $this->headerMap[strtolower($name)] ?? $name;
1,879✔
275
    }
276

277
    /**
278
     * Sets the HTTP protocol version.
279
     *
280
     * @return $this
281
     *
282
     * @throws HTTPException For invalid protocols
283
     */
284
    public function setProtocolVersion(string $version): self
285
    {
286
        // If empty, keep default protocol version (usually 1.1) and do nothing.
287
        if ($version === '') {
154✔
NEW
UNCOV
288
            return $this;
×
289
        }
290

291
        // If a full protocol string (e.g., "HTTP/1.1") is provided, extract the numeric part.
292
        if (str_contains($version, '/')) {
154✔
293
            $version = substr($version, strpos($version, '/') + 1);
69✔
294
        }
295

296
        // Normalize to a single decimal place as used in validProtocolVersions.
297
        $normalized = number_format((float) $version, 1);
154✔
298

299
        // Throw exception if the version is not recognized.
300
        if (! in_array($normalized, $this->validProtocolVersions, true)) {
154✔
301
            throw HTTPException::forInvalidHTTPProtocol($version);
1✔
302
        }
303

304
        $this->protocolVersion = $normalized;
153✔
305

306
        return $this;
153✔
307
    }
308
}
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