Skip to content

Commit c9da067

Browse files
committed
added accept header negotiations, OPTIONS method Allow header and, better method handling.
1 parent 6a2494c commit c9da067

File tree

8 files changed

+118
-18
lines changed

8 files changed

+118
-18
lines changed

flight/Engine.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
* @method Response response()
4949
* @method void error(Throwable $e)
5050
* @method void notFound()
51+
* @method void methodNotFound(Route $route)
5152
* @method void redirect(string $url, int $code = 303)
5253
* @method void json($data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0)
5354
* @method void jsonHalt($data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0)
@@ -91,6 +92,7 @@ class Engine
9192
'halt',
9293
'error',
9394
'notFound',
95+
'methodNotFound',
9496
'render',
9597
'redirect',
9698
'etag',
@@ -562,6 +564,15 @@ public function _start(): void
562564
$params[] = $route;
563565
}
564566

567+
// OPTIONS request handling
568+
if ($request->method === 'OPTIONS') {
569+
$allowedMethods = $route->methods;
570+
$response->status(204)
571+
->header('Allow', implode(', ', $allowedMethods))
572+
->send();
573+
return;
574+
}
575+
565576
// If this route is to be streamed, we need to output the headers now
566577
if ($route->is_streamed === true) {
567578
if (count($route->streamed_headers) > 0) {
@@ -644,7 +655,7 @@ public function _start(): void
644655
// Get the previous route and check if the method failed, but the URL was good.
645656
$lastRouteExecuted = $router->executedRoute;
646657
if ($lastRouteExecuted !== null && $lastRouteExecuted->matchUrl($request->url) === true && $lastRouteExecuted->matchMethod($request->method) === false) {
647-
$this->halt(405, 'Method Not Allowed', empty(getenv('PHPUNIT_TEST')));
658+
$this->methodNotFound($lastRouteExecuted);
648659
} else {
649660
$this->notFound();
650661
}
@@ -839,6 +850,19 @@ public function _notFound(): void
839850
->send();
840851
}
841852

853+
/**
854+
* Function to run if the route has been found but not the method.
855+
*
856+
* @param Route $route - The executed route
857+
*
858+
* @return void
859+
*/
860+
public function _methodNotFound(Route $route): void
861+
{
862+
$this->response()->setHeader('Allow', implode(', ', $route->methods));
863+
$this->halt(405, 'Method Not Allowed. Allowed Methods are: ' . implode(', ', $route->methods), empty(getenv('PHPUNIT_TEST')));
864+
}
865+
842866
/**
843867
* Redirects the current request to another URL.
844868
*

flight/Flight.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
* @method static void jsonp($data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512)
5656
* @method static void error(Throwable $exception)
5757
* @method static void notFound()
58+
* @method static void methodNotFound(Route $route)
5859
* @method static void etag(string $id, string $type = 'strong')
5960
* @method static void lastModified(int $time)
6061
* @method static void download(string $filePath)

flight/net/Request.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,27 @@ public static function getScheme(): string
449449
return 'http';
450450
}
451451

452+
/**
453+
* Negotiates the best content type from the Accept header.
454+
*
455+
* @param array<int, string> $supported List of supported content types.
456+
*
457+
* @return ?string The negotiated content type.
458+
*/
459+
public function negotiateContentType(array $supported): ?string
460+
{
461+
$accept = $this->header('Accept') ?? '';
462+
if ($accept === '') {
463+
return $supported[0];
464+
}
465+
foreach ($supported as $type) {
466+
if (stripos($accept, $type) !== false) {
467+
return $type;
468+
}
469+
}
470+
return null;
471+
}
472+
452473
/**
453474
* Retrieves the array of uploaded files.
454475
*

flight/net/Router.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ public function map(string $pattern, $callback, bool $pass_route = false, string
126126
if (in_array('GET', $methods, true) === true && in_array('HEAD', $methods, true) === false) {
127127
$methods[] = 'HEAD';
128128
}
129+
130+
// Always allow an OPTIONS request
131+
if (in_array('OPTIONS', $methods, true) === false) {
132+
$methods[] = 'OPTIONS';
133+
}
129134
}
130135

131136
// And this finishes it off.

tests/EngineTest.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,21 @@ public function testHeadRoute(): void
409409
$this->expectOutputString('');
410410
}
411411

412+
public function testOptionsRoute(): void
413+
{
414+
$engine = new Engine();
415+
$engine->route('GET /someRoute', function () {
416+
echo 'i ran';
417+
}, true);
418+
$engine->request()->method = 'OPTIONS';
419+
$engine->request()->url = '/someRoute';
420+
$engine->start();
421+
422+
// No body should be sent
423+
$this->expectOutputString('');
424+
$this->assertEquals('GET, HEAD, OPTIONS', $engine->response()->headers()['Allow']);
425+
}
426+
412427
public function testHalt(): void
413428
{
414429
$engine = new class extends Engine {
@@ -1070,9 +1085,10 @@ public function setRealHeader(
10701085

10711086
$engine->start();
10721087

1073-
$this->expectOutputString('Method Not Allowed');
1088+
$this->expectOutputString('Method Not Allowed. Allowed Methods are: POST, OPTIONS');
10741089
$this->assertEquals(405, $engine->response()->status());
1075-
$this->assertEquals('Method Not Allowed', $engine->response()->getBody());
1090+
$this->assertEquals('Method Not Allowed. Allowed Methods are: POST, OPTIONS', $engine->response()->getBody());
1091+
$this->assertEquals('POST, OPTIONS', $engine->response()->headers()['Allow']);
10761092
}
10771093

10781094
public function testDownload(): void

tests/RequestTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,4 +377,27 @@ public function testParseQueryWithEmptyQueryString(): void
377377
$result = Request::parseQuery('/foo?');
378378
$this->assertEquals([], $result);
379379
}
380+
381+
public function testNegotiateContentType(): void
382+
{
383+
// Find best match first
384+
$_SERVER['HTTP_ACCEPT'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8';
385+
$request = new Request();
386+
$this->assertEquals('application/xml', $request->negotiateContentType(['application/xml', 'application/json', 'text/html']));
387+
388+
// Find the first match
389+
$_SERVER['HTTP_ACCEPT'] = 'application/json,text/html';
390+
$request = new Request();
391+
$this->assertEquals('application/json', $request->negotiateContentType(['application/json', 'text/html']));
392+
393+
// No match found
394+
$_SERVER['HTTP_ACCEPT'] = 'application/xml';
395+
$request = new Request();
396+
$this->assertNull($request->negotiateContentType(['application/json', 'text/html']));
397+
398+
// No header present, return first supported type
399+
$_SERVER['HTTP_ACCEPT'] = '';
400+
$request = new Request();
401+
$this->assertEquals('application/json', $request->negotiateContentType(['application/json', 'text/html']));
402+
}
380403
}

tests/RouterTest.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ public function testGetRouteShortcut(): void
137137
public function testHeadRouteShortcut(): void
138138
{
139139
$route = $this->router->get('/path', [$this, 'ok']);
140-
$this->assertEquals(['GET', 'HEAD'], $route->methods);
140+
$this->assertEquals(['GET', 'HEAD', 'OPTIONS'], $route->methods);
141141
$this->request->url = '/path';
142142
$this->request->method = 'HEAD';
143143
$this->check('');
@@ -172,6 +172,16 @@ public function testGetPostRoute(): void
172172
$this->check('OK');
173173
}
174174

175+
public function testOptionsRouteShortcut(): void
176+
{
177+
$route = $this->router->map('GET|POST /path', [$this, 'ok']);
178+
$this->assertEquals(['GET', 'POST', 'HEAD', 'OPTIONS'], $route->methods);
179+
$this->request->url = '/path';
180+
$this->request->method = 'OPTIONS';
181+
182+
$this->check('OK');
183+
}
184+
175185
public function testPutRouteShortcut(): void
176186
{
177187
$this->router->put('/path', [$this, 'ok']);

tests/commands/RouteCommandTest.php

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,15 @@ public function testGetRoutes(): void
106106

107107
$this->assertStringContainsString('Routes', file_get_contents(static::$ou));
108108
$expected = <<<'output'
109-
+---------+-----------+-------+----------+----------------+
110-
| Pattern | Methods | Alias | Streamed | Middleware |
111-
+---------+-----------+-------+----------+----------------+
112-
| / | GET, HEAD | | No | - |
113-
| /post | POST | | No | Closure |
114-
| /delete | DELETE | | No | - |
115-
| /put | PUT | | No | - |
116-
| /patch | PATCH | | No | Bad Middleware |
117-
+---------+-----------+-------+----------+----------------+
109+
+---------+--------------------+-------+----------+----------------+
110+
| Pattern | Methods | Alias | Streamed | Middleware |
111+
+---------+--------------------+-------+----------+----------------+
112+
| / | GET, HEAD, OPTIONS | | No | - |
113+
| /post | POST, OPTIONS | | No | Closure |
114+
| /delete | DELETE, OPTIONS | | No | - |
115+
| /put | PUT, OPTIONS | | No | - |
116+
| /patch | PATCH, OPTIONS | | No | Bad Middleware |
117+
+---------+--------------------+-------+----------+----------------+
118118
output; // phpcs:ignore
119119

120120
$this->assertStringContainsString(
@@ -133,11 +133,11 @@ public function testGetPostRoute(): void
133133
$this->assertStringContainsString('Routes', file_get_contents(static::$ou));
134134

135135
$expected = <<<'output'
136-
+---------+---------+-------+----------+------------+
137-
| Pattern | Methods | Alias | Streamed | Middleware |
138-
+---------+---------+-------+----------+------------+
139-
| /post | POST | | No | Closure |
140-
+---------+---------+-------+----------+------------+
136+
+---------+---------------+-------+----------+------------+
137+
| Pattern | Methods | Alias | Streamed | Middleware |
138+
+---------+---------------+-------+----------+------------+
139+
| /post | POST, OPTIONS | | No | Closure |
140+
+---------+---------------+-------+----------+------------+
141141
output; // phpcs:ignore
142142

143143
$this->assertStringContainsString(

0 commit comments

Comments
 (0)