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

codeigniter4 / CodeIgniter4 / 28958516395

08 Jul 2026 04:24PM UTC coverage: 89.593% (+0.005%) from 89.588%
28958516395

Pull #10395

github

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

30 of 40 new or added lines in 3 files covered. (75.0%)

25294 of 28232 relevant lines covered (89.59%)

230.43 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();
27✔
41

42
        $this->callback = is_callable($callbackOrChunks)
27✔
43
            ? $callbackOrChunks(...)
24✔
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()) {
7✔
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) {
7✔
NEW
121
            session_write_close();
×
122
        }
123

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

126
        // Intentionally skip CSP finalize: the body is streamed, not buffered HTML.
127
        $this->sendHeaders();
7✔
128
        $this->sendCookies();
7✔
129

130
        ($this->callback)($this);
7✔
131

132
        return $this;
7✔
133
    }
134

135
    /**
136
     * Applies headers needed for unbuffered delivery, without overriding
137
     * any the developer has already set.
138
     */
139
    protected function prepareStreamHeaders(): void
140
    {
141
        if (! $this->hasHeader('X-Accel-Buffering')) {
6✔
142
            $this->setHeader('X-Accel-Buffering', 'no');
5✔
143
        }
144

145
        if (! $this->hasHeader('Content-Encoding')) {
6✔
146
            $this->setHeader('Content-Encoding', 'identity');
6✔
147
        }
148
    }
149

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