From 87488db45f0a48ebe026188c2bc341c5ad5d052c Mon Sep 17 00:00:00 2001 From: Christoph Kempen Date: Sun, 25 Oct 2026 23:00:17 +0100 Subject: [PATCH] fix: return 304 only when the file is unchanged since If-Modified-Since --- src/DocumentRoot.php | 5 ++-- test/DocumentRootTest.php | 57 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/DocumentRoot.php b/src/DocumentRoot.php index a05a822..46548d2 100644 --- a/src/DocumentRoot.php +++ b/src/DocumentRoot.php @@ -306,9 +306,10 @@ private function checkPreconditions(Request $request, int $mtime, string $etag): return Precondition::NotModified; } + // If-Modified-Since must be ignored when If-None-Match is present (RFC 9110 section 13.1.3) $ifModifiedSince = $request->getHeader("If-Modified-Since"); - $ifModifiedSince = $ifModifiedSince ? \strtotime($ifModifiedSince) : 0; - if ($ifModifiedSince && $mtime > $ifModifiedSince) { + $ifModifiedSince = $ifModifiedSince && !$ifNoneMatch ? \strtotime($ifModifiedSince) : 0; + if ($ifModifiedSince && $mtime <= $ifModifiedSince) { return Precondition::NotModified; } diff --git a/test/DocumentRootTest.php b/test/DocumentRootTest.php index f367f4b..6eaa7d7 100644 --- a/test/DocumentRootTest.php +++ b/test/DocumentRootTest.php @@ -250,7 +250,7 @@ public function testPreconditionNotModified(): void $request = new Request($this->createMock(Client::class), "GET", $this->createUri("/index.htm"), [ "if-match" => $etag, - "if-modified-since" => "2.1.1970", + "if-modified-since" => \gmdate("D, d M Y H:i:s", \filemtime($diskPath))." GMT", ]); $response = $this->root->handleRequest($request); @@ -260,6 +260,61 @@ public function testPreconditionNotModified(): void $this->assertSame($etag, $response->getHeader("etag")); } + public function testIfModifiedSinceOlderThanMtimeReturnsFile(): void + { + $diskPath = \realpath(self::fixturePath())."/index.htm"; + + $request = new Request($this->createMock(Client::class), "GET", $this->createUri("/index.htm"), [ + "if-modified-since" => \gmdate("D, d M Y H:i:s", \filemtime($diskPath) - 3600)." GMT", + ]); + + $response = $this->root->handleRequest($request); + + $this->assertSame(HttpStatus::OK, $response->getStatus()); + $this->assertSame(\file_get_contents($diskPath), $response->getBody()->read()); + } + + public function testIfModifiedSinceEqualToMtimeReturnsNotModified(): void + { + $diskPath = \realpath(self::fixturePath())."/index.htm"; + + $request = new Request($this->createMock(Client::class), "GET", $this->createUri("/index.htm"), [ + "if-modified-since" => \gmdate("D, d M Y H:i:s", \filemtime($diskPath))." GMT", + ]); + + $response = $this->root->handleRequest($request); + + $this->assertSame(HttpStatus::NOT_MODIFIED, $response->getStatus()); + } + + public function testIfModifiedSinceNewerThanMtimeReturnsNotModified(): void + { + $diskPath = \realpath(self::fixturePath())."/index.htm"; + + $request = new Request($this->createMock(Client::class), "GET", $this->createUri("/index.htm"), [ + "if-modified-since" => \gmdate("D, d M Y H:i:s", \filemtime($diskPath) + 3600)." GMT", + ]); + + $response = $this->root->handleRequest($request); + + $this->assertSame(HttpStatus::NOT_MODIFIED, $response->getStatus()); + } + + public function testIfModifiedSinceIsIgnoredWhenIfNoneMatchDoesNotMatch(): void + { + $diskPath = \realpath(self::fixturePath())."/index.htm"; + + $request = new Request($this->createMock(Client::class), "GET", $this->createUri("/index.htm"), [ + "if-none-match" => "\"not-the-etag\"", + "if-modified-since" => \gmdate("D, d M Y H:i:s", \filemtime($diskPath) + 3600)." GMT", + ]); + + $response = $this->root->handleRequest($request); + + $this->assertSame(HttpStatus::OK, $response->getStatus()); + $this->assertSame(\file_get_contents($diskPath), $response->getBody()->read()); + } + public function testPreconditionRangeFail(): void { $this->root->setUseEtagInode(false);