33namespace Utopia \Http \Adapter \Swoole ;
44
55use Swoole \Http \Response as SwooleResponse ;
6+ use Swoole \Http \Server as SwooleServer ;
67use Utopia \Http \Response as UtopiaResponse ;
78
89class Response extends UtopiaResponse
@@ -14,6 +15,13 @@ class Response extends UtopiaResponse
1415 */
1516 protected SwooleResponse $ swoole ;
1617
18+ /**
19+ * Swoole Server Object (needed for raw TCP streaming with Content-Length)
20+ *
21+ * @var SwooleServer|null
22+ */
23+ protected ?SwooleServer $ server = null ;
24+
1725 /**
1826 * Response constructor.
1927 */
@@ -23,6 +31,19 @@ public function __construct(SwooleResponse $response)
2331 parent ::__construct (\microtime (true ));
2432 }
2533
34+ /**
35+ * Set the Swoole server instance for raw TCP streaming.
36+ *
37+ * @param SwooleServer $server
38+ * @return static
39+ */
40+ public function setServer (SwooleServer $ server ): static
41+ {
42+ $ this ->server = $ server ;
43+
44+ return $ this ;
45+ }
46+
2647 /**
2748 * Write
2849 *
@@ -45,6 +66,125 @@ public function end(?string $content = null): void
4566 $ this ->swoole ->end ($ content );
4667 }
4768
69+ /**
70+ * Stream response
71+ *
72+ * Uses detach() + $server->send() for raw TCP streaming that preserves
73+ * Content-Length (so browsers show download progress). Falls back to
74+ * the base class implementation if no server instance is available.
75+ *
76+ * @param callable|\Generator $source Either a callable($offset, $length) or a Generator yielding string chunks
77+ * @param int $totalSize Total size of the content in bytes
78+ * @return void
79+ */
80+ public function stream (callable |\Generator $ source , int $ totalSize ): void
81+ {
82+ if ($ this ->sent ) {
83+ return ;
84+ }
85+
86+ $ this ->sent = true ;
87+
88+ if ($ this ->server === null ) {
89+ $ this ->sent = false ;
90+ parent ::stream ($ source , $ totalSize );
91+
92+ return ;
93+ }
94+
95+ if ($ this ->disablePayload ) {
96+ $ this ->appendCookies ();
97+ $ this ->appendHeaders ();
98+ $ this ->swoole ->end ();
99+ $ this ->disablePayload ();
100+
101+ return ;
102+ }
103+
104+ // Build raw HTTP response headers for direct TCP send
105+ $ this ->addHeader ('Content-Length ' , (string ) $ totalSize , override: true );
106+ $ this ->addHeader ('Connection ' , 'close ' , override: true );
107+ $ this ->addHeader ('X-Debug-Speed ' , (string ) (microtime (true ) - $ this ->startTime ), override: true );
108+
109+ if (!empty ($ this ->contentType )) {
110+ $ this ->addHeader ('Content-Type ' , $ this ->contentType , override: true );
111+ }
112+
113+ $ statusReason = $ this ->getStatusCodeReason ($ this ->statusCode );
114+ $ rawHeaders = "HTTP/1.1 {$ this ->statusCode } {$ statusReason }\r\n" ;
115+
116+ foreach ($ this ->headers as $ key => $ value ) {
117+ if (\is_array ($ value )) {
118+ foreach ($ value as $ v ) {
119+ $ rawHeaders .= "{$ key }: {$ v }\r\n" ;
120+ }
121+ } else {
122+ $ rawHeaders .= "{$ key }: {$ value }\r\n" ;
123+ }
124+ }
125+
126+ foreach ($ this ->cookies as $ cookie ) {
127+ $ cookieStr = \urlencode ($ cookie ['name ' ]) . '= ' . \urlencode ($ cookie ['value ' ] ?? '' );
128+ if (!empty ($ cookie ['expire ' ])) {
129+ $ cookieStr .= '; Expires= ' . \gmdate ('D, d M Y H:i:s T ' , $ cookie ['expire ' ]);
130+ }
131+ if (!empty ($ cookie ['path ' ])) {
132+ $ cookieStr .= '; Path= ' . $ cookie ['path ' ];
133+ }
134+ if (!empty ($ cookie ['domain ' ])) {
135+ $ cookieStr .= '; Domain= ' . $ cookie ['domain ' ];
136+ }
137+ if (!empty ($ cookie ['secure ' ])) {
138+ $ cookieStr .= '; Secure ' ;
139+ }
140+ if (!empty ($ cookie ['httponly ' ])) {
141+ $ cookieStr .= '; HttpOnly ' ;
142+ }
143+ if (!empty ($ cookie ['samesite ' ])) {
144+ $ cookieStr .= '; SameSite= ' . $ cookie ['samesite ' ];
145+ }
146+ $ rawHeaders .= "Set-Cookie: {$ cookieStr }\r\n" ;
147+ }
148+
149+ $ rawHeaders .= "\r\n" ;
150+
151+ // Detach from Swoole HTTP layer and send raw TCP
152+ $ fd = $ this ->swoole ->fd ;
153+ $ this ->swoole ->detach ();
154+
155+ if ($ this ->server ->send ($ fd , $ rawHeaders ) === false ) {
156+ $ this ->disablePayload ();
157+
158+ return ;
159+ }
160+
161+ // Stream body chunks
162+ if ($ source instanceof \Generator) {
163+ foreach ($ source as $ chunk ) {
164+ if (!empty ($ chunk )) {
165+ $ this ->size += strlen ($ chunk );
166+ if ($ this ->server ->send ($ fd , $ chunk ) === false ) {
167+ break ;
168+ }
169+ }
170+ }
171+ } else {
172+ $ length = self ::CHUNK_SIZE ;
173+ for ($ offset = 0 ; $ offset < $ totalSize ; $ offset += $ length ) {
174+ $ chunk = $ source ($ offset , min ($ length , $ totalSize - $ offset ));
175+ if (!empty ($ chunk )) {
176+ $ this ->size += strlen ($ chunk );
177+ if ($ this ->server ->send ($ fd , $ chunk ) === false ) {
178+ break ;
179+ }
180+ }
181+ }
182+ }
183+
184+ $ this ->server ->close ($ fd );
185+ $ this ->disablePayload ();
186+ }
187+
48188 /**
49189 * Get status code reason
50190 *
0 commit comments