Skip to content

Commit 37ee48d

Browse files
committed
Test Swoole response header value ownership
Exercise the full Hypervel response path over HTTP/1 and HTTP/2 with a dynamically built header value. Record the response after Swoole serializes it so the regression detects mutation of caller-owned string storage. Skip the regressions on Swoole 6.3.0RC1 while retaining the exact expected value for releases containing the upstream fix.
1 parent 0e623b1 commit 37ee48d

2 files changed

Lines changed: 83 additions & 0 deletions

File tree

‎tests/Integration/HttpServer/Fixtures/server.php‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Hypervel\HttpServer\Events\RequestHandled;
1414
use Hypervel\HttpServer\Events\RequestReceived;
1515
use Hypervel\HttpServer\Events\RequestTerminated;
16+
use Hypervel\HttpServer\Events\ResponseSent;
1617
use Hypervel\HttpServer\Server as HttpServer;
1718
use Hypervel\Routing\Router;
1819
use Hypervel\Server\Event;
@@ -125,6 +126,18 @@
125126
: null,
126127
];
127128
});
129+
$events->listen(ResponseSent::class, function (ResponseSent $event) use ($state): void {
130+
$token = $event->request?->query('token');
131+
132+
if ($event->request?->getPathInfo() !== '/response-header'
133+
|| ! is_string($token)
134+
|| $token === ''
135+
) {
136+
return;
137+
}
138+
139+
$state->responseHeaders[$token] = bin2hex($event->response->headers->get('X-Test'));
140+
});
128141

129142
$filesystem = new Filesystem;
130143
$filesystem->ensureDirectoryExists(storage_path('app/private'));
@@ -181,6 +194,24 @@
181194

182195
return $response;
183196
});
197+
$router->get('/response-header', function (): Response {
198+
// Build the value at runtime so it has the same refcounted storage as application header values.
199+
$value = implode('', ['value', "\t "]);
200+
201+
return new Response('response header', headers: ['X-Test' => $value]);
202+
});
203+
$router->get('/response-header-state', function (Request $request) use ($state): JsonResponse {
204+
$token = $request->query('token');
205+
206+
if (! is_string($token) || $token === '') {
207+
return new JsonResponse([]);
208+
}
209+
210+
$value = $state->responseHeaders[$token] ?? null;
211+
unset($state->responseHeaders[$token]);
212+
213+
return new JsonResponse(['value' => $value]);
214+
});
184215
$router->get('/callback-stream', fn (): Response => response()->stream(function (): void {
185216
echo 'callback-';
186217
echo 'stream';
@@ -268,6 +299,11 @@ class HttpServerIntegrationState
268299

269300
public ?string $deletePath = null;
270301

302+
/**
303+
* @var array<string, string>
304+
*/
305+
public array $responseHeaders = [];
306+
271307
/**
272308
* @var array<string, array<string, mixed>>
273309
*/
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypervel\Tests\Integration\HttpServer;
6+
7+
use Hypervel\Engine\Http\V2\Client as Http2Client;
8+
use Hypervel\Engine\Http\V2\Request as Http2Request;
9+
10+
class ResponseSerializationTest extends HttpServerIntegrationTestCase
11+
{
12+
public function testResponseSerializationDoesNotMutateHeaderValues(): void
13+
{
14+
// @TODO: Remove this skip once Swoole preserves response header values during serialization.
15+
if (version_compare(SWOOLE_VERSION, '6.3.0RC1', '<=')) {
16+
$this->markTestSkipped('Swoole mutates caller-owned response header values while serializing them.');
17+
}
18+
19+
$token = bin2hex(random_bytes(8));
20+
$this->request('GET', '/response-header?token=' . $token);
21+
$state = $this->decode($this->request('GET', '/response-header-state?token=' . $token));
22+
23+
$this->assertSame(bin2hex("value\t "), $state['value']);
24+
}
25+
26+
public function testHttpTwoResponseSerializationDoesNotMutateHeaderValues(): void
27+
{
28+
// @TODO: Remove this skip once Swoole preserves response header values during serialization.
29+
if (version_compare(SWOOLE_VERSION, '6.3.0RC1', '<=')) {
30+
$this->markTestSkipped('Swoole mutates caller-owned response header values while serializing them.');
31+
}
32+
33+
$token = bin2hex(random_bytes(8));
34+
$client = new Http2Client($this->getServerHost(), $this->getServerPort());
35+
36+
try {
37+
$client->send(new Http2Request('/response-header?token=' . $token));
38+
$response = $client->recv(5);
39+
} finally {
40+
$client->close();
41+
}
42+
43+
$this->assertNotNull($response);
44+
$state = $this->decode($this->request('GET', '/response-header-state?token=' . $token));
45+
$this->assertSame(bin2hex("value\t "), $state['value']);
46+
}
47+
}

0 commit comments

Comments
 (0)