Skip to content

Commit f912d6a

Browse files
authored
fix(router): support implicit HEAD requests (#1349)
1 parent 42f78e9 commit f912d6a

File tree

9 files changed

+135
-6
lines changed

9 files changed

+135
-6
lines changed

packages/http/src/IsRequest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,13 @@ public function hasQuery(string $key): bool
135135
{
136136
return has_key($this->query, $key);
137137
}
138+
139+
public function withMethod(Method $method): self
140+
{
141+
$clone = clone $this;
142+
143+
$clone->method = $method;
144+
145+
return $clone;
146+
}
138147
}

packages/router/src/GenericResponseSender.php

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
namespace Tempest\Router;
66

77
use Generator;
8+
use Tempest\Container\Container;
89
use Tempest\Http\ContentType;
910
use Tempest\Http\Header;
11+
use Tempest\Http\Method;
12+
use Tempest\Http\Request;
1013
use Tempest\Http\Response;
1114
use Tempest\Http\Responses\Download;
1215
use Tempest\Http\Responses\EventStream;
@@ -19,15 +22,20 @@
1922
final readonly class GenericResponseSender implements ResponseSender
2023
{
2124
public function __construct(
25+
private Container $container,
2226
private ViewRenderer $viewRenderer,
2327
) {}
2428

2529
public function send(Response $response): Response
2630
{
2731
ob_start();
2832
$this->sendHeaders($response);
29-
ob_flush();
30-
$this->sendContent($response);
33+
34+
if ($this->shouldSendContent()) {
35+
ob_flush();
36+
$this->sendContent($response);
37+
}
38+
3139
ob_end_flush();
3240

3341
if (function_exists('fastcgi_finish_request')) {
@@ -67,6 +75,16 @@ private function resolveHeaders(Response $response): Generator
6775
}
6876
}
6977

78+
private function shouldSendContent(): bool
79+
{
80+
// The request is resolved dynamically from the container
81+
// because it's only available via the container at a later point,
82+
// after the response sender has been constructed (set by the router)
83+
$request = $this->container->get(Request::class);
84+
85+
return $request->method !== Method::HEAD;
86+
}
87+
7088
private function sendContent(Response $response): void
7189
{
7290
if ($response instanceof EventStream) {

packages/router/src/HttpApplication.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,13 @@
66

77
use Tempest\Container\Container;
88
use Tempest\Container\Singleton;
9-
use Tempest\Core\AppConfig;
109
use Tempest\Core\Application;
1110
use Tempest\Core\Kernel;
1211
use Tempest\Core\Tempest;
1312
use Tempest\Http\RequestFactory;
1413
use Tempest\Http\Session\Session;
1514
use Tempest\Log\Channels\AppendLogChannel;
1615
use Tempest\Log\LogConfig;
17-
use Throwable;
1816

1917
use function Tempest\env;
2018
use function Tempest\Support\path;

packages/router/src/MatchRouteMiddleware.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Tempest\Core\Priority;
77
use Tempest\Http\GenericRequest;
88
use Tempest\Http\Mappers\RequestToObjectMapper;
9+
use Tempest\Http\Method;
910
use Tempest\Http\Request;
1011
use Tempest\Http\Response;
1112
use Tempest\Http\Responses\NotFound;
@@ -25,6 +26,10 @@ public function __invoke(Request $request, HttpMiddlewareCallable $next): Respon
2526
{
2627
$matchedRoute = $this->routeMatcher->match($request);
2728

29+
if ($matchedRoute === null && $request->method === Method::HEAD && $request instanceof GenericRequest) {
30+
$matchedRoute = $this->routeMatcher->match($request->withMethod(Method::GET));
31+
}
32+
2833
if ($matchedRoute === null) {
2934
return new NotFound();
3035
}

packages/router/src/ResponseSenderInitializer.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
namespace Tempest\Router;
66

7-
use Tempest\Clock\Clock;
87
use Tempest\Container\Container;
98
use Tempest\Container\Initializer;
109
use Tempest\Container\Singleton;
@@ -15,6 +14,9 @@ final class ResponseSenderInitializer implements Initializer
1514
#[Singleton]
1615
public function initialize(Container $container): ResponseSender
1716
{
18-
return new GenericResponseSender($container->get(ViewRenderer::class));
17+
return new GenericResponseSender(
18+
$container,
19+
$container->get(ViewRenderer::class),
20+
);
1921
}
2022
}

tests/Integration/FrameworkIntegrationTestCase.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,29 @@
66

77
use Closure;
88
use InvalidArgumentException;
9+
use Tempest\Cache\Testing\CacheTester;
910
use Tempest\Console\ConsoleApplication;
1011
use Tempest\Console\Input\ConsoleArgumentBag;
1112
use Tempest\Console\Output\MemoryOutputBuffer;
1213
use Tempest\Console\Output\StdoutOutputBuffer;
1314
use Tempest\Console\OutputBuffer;
1415
use Tempest\Console\Testing\ConsoleTester;
16+
use Tempest\Container\GenericContainer;
17+
use Tempest\Core\AppConfig;
1518
use Tempest\Core\Application;
19+
use Tempest\Core\ExceptionTester;
20+
use Tempest\Core\Kernel;
1621
use Tempest\Core\ShellExecutor;
1722
use Tempest\Core\ShellExecutors\NullShellExecutor;
1823
use Tempest\Database\Connection\ConnectionInitializer;
1924
use Tempest\Database\DatabaseInitializer;
2025
use Tempest\Database\Migrations\MigrationManager;
2126
use Tempest\Discovery\DiscoveryLocation;
27+
use Tempest\EventBus\Testing\EventBusTester;
28+
use Tempest\Framework\Testing\Http\HttpRouterTester;
29+
use Tempest\Framework\Testing\InstallerTester;
2230
use Tempest\Framework\Testing\IntegrationTest;
31+
use Tempest\Framework\Testing\ViteTester;
2332
use Tempest\Reflection\MethodReflector;
2433
use Tempest\Router\HttpApplication;
2534
use Tempest\Router\Route;
@@ -28,6 +37,7 @@
2837
use Tempest\Router\Routing\Construction\RouteConfigurator;
2938
use Tempest\Router\Static\StaticPageConfig;
3039
use Tempest\Router\StaticPage;
40+
use Tempest\Storage\Testing\StorageTester;
3141
use Tempest\View\Components\AnonymousViewComponent;
3242
use Tempest\View\GenericView;
3343
use Tempest\View\View;
@@ -37,6 +47,20 @@
3747

3848
use function Tempest\Support\Path\normalize;
3949

50+
/**
51+
* Added these properties because of an autocompletion bug in PhpStorm
52+
* @property AppConfig $appConfig
53+
* @property Kernel $kernel
54+
* @property GenericContainer $container
55+
* @property ConsoleTester $console
56+
* @property HttpRouterTester $http
57+
* @property InstallerTester $installer
58+
* @property ViteTester $vite
59+
* @property EventBusTester $eventBus
60+
* @property StorageTester $storage
61+
* @property CacheTester $cache
62+
* @property ExceptionTester $exceptions
63+
*/
4064
abstract class FrameworkIntegrationTestCase extends IntegrationTest
4165
{
4266
protected function setUp(): void

tests/Integration/Http/GenericResponseSenderTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@
44

55
namespace Tests\Tempest\Integration\Http;
66

7+
use Tempest\Http\GenericRequest;
78
use Tempest\Http\GenericResponse;
9+
use Tempest\Http\Method;
10+
use Tempest\Http\Request;
811
use Tempest\Http\Responses\Download;
912
use Tempest\Http\Responses\EventStream;
1013
use Tempest\Http\Responses\File;
1114
use Tempest\Http\Responses\Ok;
1215
use Tempest\Http\ServerSentEvent;
1316
use Tempest\Http\Status;
1417
use Tempest\Router\GenericResponseSender;
18+
use Tempest\View\ViewRenderer;
1519
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
1620

1721
use function Tempest\view;
@@ -38,6 +42,35 @@ public function test_sending(): void
3842
ob_get_clean();
3943
}
4044

45+
public function test_sending_head_request(): void
46+
{
47+
$request = new GenericRequest(
48+
method: Method::HEAD,
49+
uri: '/test',
50+
);
51+
52+
$this->container->singleton(Request::class, $request);
53+
54+
$responseSender = new GenericResponseSender(
55+
$this->container,
56+
$this->container->get(ViewRenderer::class),
57+
);
58+
59+
$response = new GenericResponse(
60+
status: Status::OK,
61+
body: 'body',
62+
headers: ['x-custom' => ['true']],
63+
);
64+
65+
ob_start();
66+
67+
$responseSender->send($response);
68+
69+
$content = ob_get_clean();
70+
71+
$this->assertSame('', $content);
72+
}
73+
4174
public function test_file_response(): void
4275
{
4376
ob_start();
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Tests\Tempest\Integration\Route\Fixtures;
4+
5+
use Tempest\Http\Response;
6+
use Tempest\Http\Responses\Ok;
7+
use Tempest\Router\Get;
8+
use Tempest\Router\Head;
9+
10+
final class HeadController
11+
{
12+
#[Get('/implicit-head')]
13+
public function implicitHead(): Response
14+
{
15+
return new Ok('body')->addHeader('x-custom', 'true');
16+
}
17+
18+
#[Head('/explicit-head')]
19+
public function explicitHead(): Response
20+
{
21+
return new Ok()->addHeader('x-custom', 'true');
22+
}
23+
}

tests/Integration/Route/RouterTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use Tests\Tempest\Fixtures\Modules\Books\Models\Author;
2929
use Tests\Tempest\Fixtures\Modules\Books\Models\Book;
3030
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
31+
use Tests\Tempest\Integration\Route\Fixtures\HeadController;
3132
use Tests\Tempest\Integration\Route\Fixtures\Http500Controller;
3233

3334
use function Tempest\uri;
@@ -334,4 +335,20 @@ public function test_converts_to_response(): void
334335
->assertStatus(Status::FOUND)
335336
->assertHeaderContains('Location', 'https://tempestphp.com');
336337
}
338+
339+
public function test_head_requests(): void
340+
{
341+
$this->registerRoute([HeadController::class, 'implicitHead']);
342+
$this->registerRoute([HeadController::class, 'explicitHead']);
343+
344+
$this->http
345+
->head('/implicit-head')
346+
->assertOk()
347+
->assertHasHeader('x-custom');
348+
349+
$this->http
350+
->head('/explicit-head')
351+
->assertOk()
352+
->assertHasHeader('x-custom');
353+
}
337354
}

0 commit comments

Comments
 (0)