diff --git a/formwork/src/Http/FileResponse.php b/formwork/src/Http/FileResponse.php index 75ee06d9..fdf8cb29 100644 --- a/formwork/src/Http/FileResponse.php +++ b/formwork/src/Http/FileResponse.php @@ -41,6 +41,12 @@ public function send(): void $this->sendHeaders(); + $length = $this->length ?? $this->fileSize; + + if ($length === 0) { + return; + } + $file = fopen($this->path, 'r'); $output = fopen('php://output', 'w'); @@ -58,8 +64,6 @@ public function send(): void fseek($file, $this->offset); } - $length = $this->length ?? $this->fileSize; - while ($length > 0 && !feof($file)) { $read = fread($file, self::CHUNK_SIZE); @@ -84,10 +88,15 @@ public function prepare(Request $request): static { parent::prepare($request); - if (!isset($this->headers['Accept-Ranges']) && $request->method() === RequestMethod::GET) { + if (!isset($this->headers['Accept-Ranges']) && in_array($request->method(), [RequestMethod::HEAD, RequestMethod::GET], true)) { $this->headers['Accept-Ranges'] = 'bytes'; } + if ($request->method() === RequestMethod::HEAD || $this->requiresEmptyContent()) { + $this->length = 0; + return $this; + } + if ($request->method() === RequestMethod::GET && preg_match('/^bytes=(\d+)?-(\d+)?$/', $request->headers()->get('Range', ''), $matches, PREG_UNMATCHED_AS_NULL)) { [, $start, $end] = $matches; diff --git a/formwork/src/Http/RequestMethod.php b/formwork/src/Http/RequestMethod.php index d7232454..381439b4 100644 --- a/formwork/src/Http/RequestMethod.php +++ b/formwork/src/Http/RequestMethod.php @@ -4,6 +4,7 @@ enum RequestMethod: string { + case HEAD = 'HEAD'; case GET = 'GET'; case POST = 'POST'; case PUT = 'PUT'; diff --git a/formwork/src/Http/Response.php b/formwork/src/Http/Response.php index 4c7f198d..0979e681 100644 --- a/formwork/src/Http/Response.php +++ b/formwork/src/Http/Response.php @@ -22,7 +22,8 @@ class Response implements ResponseInterface public function __construct(protected string $content, protected ResponseStatus $responseStatus = ResponseStatus::OK, array $headers = []) { $headers += [ - 'Content-Type' => Header::make(['text/html', 'charset' => 'utf-8']), + 'Content-Length' => (string) strlen($content), + 'Content-Type' => Header::make(['text/html', 'charset' => 'utf-8']), ]; $this->headers = $headers; } @@ -61,6 +62,10 @@ public function headers(): array */ public function prepare(Request $request): static { + if ($request->method() === RequestMethod::HEAD || $this->requiresEmptyContent()) { + $this->content = ''; + } + return $this; } @@ -116,4 +121,9 @@ public static function cleanOutputBuffers(): void ob_end_clean(); } } + + protected function requiresEmptyContent(): bool + { + return in_array($this->responseStatus, [ResponseStatus::NoContent, ResponseStatus::NotModified], true); + } } diff --git a/formwork/src/Router/Router.php b/formwork/src/Router/Router.php index b3374a32..034ebf90 100644 --- a/formwork/src/Router/Router.php +++ b/formwork/src/Router/Router.php @@ -4,6 +4,7 @@ use Closure; use Formwork\Http\Request; +use Formwork\Http\RequestMethod; use Formwork\Http\Response; use Formwork\Parsers\Php; use Formwork\Router\Exceptions\InvalidRouteException; @@ -26,7 +27,7 @@ class Router /** * Valid request methods */ - protected const REQUEST_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']; + protected const REQUEST_METHODS = ['HEAD', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE']; /** * Valid param separators @@ -449,7 +450,14 @@ protected function validateParamName(?string $param, array $params): string */ protected function matchMethods(array $methods): bool { - return in_array($this->request->method()->value, $methods, true); + $method = $this->request->method(); + + // HEAD method is equivalent to GET method + if ($method === RequestMethod::HEAD) { + $method = RequestMethod::GET; + } + + return in_array($method->value, $methods, true); } /**