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

codeigniter4 / CodeIgniter4 / 28971241396

08 Jul 2026 07:51PM UTC coverage: 89.593% (+0.005%) from 89.588%
28971241396

Pull #10395

github

web-flow
Merge 4721eeebf into 18ee79cfa
Pull Request #10395: feat: add StreamResponse and SSE response factories

31 of 41 new or added lines in 3 files covered. (75.61%)

25294 of 28232 relevant lines covered (89.59%)

230.48 hits per line

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

72.22
/system/HTTP/StreamResponse.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 Closure;
17

18
/**
19
 * HTTP response that streams its body to the client as it is generated,
20
 * instead of buffering the complete body first.
21
 *
22
 * @see \CodeIgniter\HTTP\StreamResponseTest
23
 */
24
class StreamResponse extends Response implements NonBufferedResponseInterface
25
{
26
    /**
27
     * @var Closure(static): void
28
     */
29
    private readonly Closure $callback;
30

31
    /**
32
     * @param (callable(static): void)|iterable<string> $callbackOrChunks A callback that
33
     *                                                                    streams output via write(), or an iterable of
34
     *                                                                    string chunks to be written in order. A value
35
     *                                                                    that is both callable and iterable is treated
36
     *                                                                    as a callback.
37
     */
38
    public function __construct(callable|iterable $callbackOrChunks)
39
    {
40
        parent::__construct();
29✔
41

42
        $this->callback = is_callable($callbackOrChunks)
29✔
43
            ? $callbackOrChunks(...)
26✔
44
            : static function (self $response) use ($callbackOrChunks): void {
3✔
45
                foreach ($callbackOrChunks as $chunk) {
2✔
46
                    if (! $response->write($chunk)) {
2✔
NEW
47
                        break;
×
48
                    }
49
                }
50
            };
3✔
51
    }
52

53
    /**
54
     * Write a chunk of the streamed body.
55
     *
56
     * @param bool $flush Whether to flush output to the client immediately.
57
     *                    Pass false when writing many small chunks, then call
58
     *                    flush() at intervals.
59
     *
60
     * @return bool false if the client has disconnected
61
     */
62
    public function write(string $chunk, bool $flush = true): bool
63
    {
64
        if (! $this->isClientConnected()) {
18✔
NEW
65
            return false;
×
66
        }
67

68
        echo $chunk;
18✔
69

70
        if ($flush) {
18✔
71
            $this->flush();
17✔
72
        }
73

74
        return true;
18✔
75
    }
76

77
    /**
78
     * Flush buffered output to the client.
79
     */
80
    public function flush(): void
81
    {
82
        if (! service('environment')->isTesting()) {
17✔
NEW
83
            if (ob_get_level() > 0) {
×
NEW
84
                ob_flush();
×
85
            }
86

NEW
87
            flush();
×
88
        }
89
    }
90

91
    /**
92
     * Whether the client connection is still open.
93
     *
94
     * Note: PHP only detects a disconnect when attempting to write,
95
     * so this may return true until the next write() call fails.
96
     */
97
    public function isClientConnected(): bool
98
    {
99
        return connection_status() === CONNECTION_NORMAL && connection_aborted() === 0;
20✔
100
    }
101

102
    /**
103
     * {@inheritDoc}
104
     *
105
     * @return $this
106
     */
107
    public function send()
108
    {
109
        // Turn off output buffering completely, even if php.ini output_buffering is not off
110
        if (! service('environment')->isTesting()) {
9✔
NEW
111
            set_time_limit(0);
×
NEW
112
            ini_set('zlib.output_compression', 'Off');
×
113

NEW
114
            while (ob_get_level() > 0) {
×
NEW
115
                ob_end_clean();
×
116
            }
117
        }
118

119
        // Close session if active to prevent blocking other requests
120
        if (session_status() === PHP_SESSION_ACTIVE) {
9✔
NEW
121
            session_write_close();
×
122
        }
123

124
        $this->prepareStreamHeaders();
9✔
125

126
        // Give CSP a chance to build its headers. Nonce placeholders cannot be
127
        // replaced in a streamed body; call getCSP()->getScriptNonce() or
128
        // getStyleNonce() before returning the response instead.
129
        if ($this->shouldFinalizeCsp()) {
9✔
130
            $this->getCSP()->finalize($this);
6✔
131
        }
132

133
        $this->sendHeaders();
9✔
134
        $this->sendCookies();
9✔
135

136
        ($this->callback)($this);
9✔
137

138
        return $this;
9✔
139
    }
140

141
    /**
142
     * Applies headers needed for unbuffered delivery, without overriding
143
     * any the developer has already set.
144
     */
145
    protected function prepareStreamHeaders(): void
146
    {
147
        if (! $this->hasHeader('X-Accel-Buffering')) {
7✔
148
            $this->setHeader('X-Accel-Buffering', 'no');
6✔
149
        }
150
    }
151

152
    /**
153
     * {@inheritDoc}
154
     *
155
     * No-op — body is streamed via the callback, not stored.
156
     *
157
     * @return $this
158
     */
159
    public function sendBody()
160
    {
161
        return $this;
2✔
162
    }
163
}
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