From ccb2dad40b49f8217670be7d8416b67a327923d9 Mon Sep 17 00:00:00 2001 From: Ian Gordon Date: Tue, 28 Apr 2026 11:28:31 -0400 Subject: [PATCH 1/2] Stream DirectoryHTTPHandler responses via HTTPBodySequence(file:) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Matches FileHTTPHandler so a multi-gigabyte asset under a directory handler no longer loads fully into memory per request. Existence-check moves into the HTTPBodySequence(file:) throw, mirroring FileHTTPHandler's do/catch → 404 pattern. Closes TVT-286 Co-Authored-By: Claude Opus 4.7 --- .../Handlers/DirectoryHTTPHandler.swift | 52 ++++++++++--------- .../Handlers/DirectoryHTTPHandlerTests.swift | 12 +++++ 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/FlyingFox/Sources/Handlers/DirectoryHTTPHandler.swift b/FlyingFox/Sources/Handlers/DirectoryHTTPHandler.swift index 910cf593..fd35d5ac 100644 --- a/FlyingFox/Sources/Handlers/DirectoryHTTPHandler.swift +++ b/FlyingFox/Sources/Handlers/DirectoryHTTPHandler.swift @@ -55,39 +55,41 @@ public struct DirectoryHTTPHandler: HTTPHandler { } public func handleRequest(_ request: HTTPRequest) async throws -> HTTPResponse { - guard - let filePath = makeFileURL(for: request.path), - let data = try? Data(contentsOf: filePath) else { + guard let filePath = makeFileURL(for: request.path) else { return HTTPResponse(statusCode: .notFound) } - var headers: HTTPHeaders = [ - .contentType: FileHTTPHandler.makeContentType(for: filePath.absoluteString), - .cacheControl: cacheControl.getSerializedValue(), - .date: HTTPCacheControl.getDateHeaderValue() - ] + do { + var headers: HTTPHeaders = [ + .contentType: FileHTTPHandler.makeContentType(for: filePath.absoluteString), + .cacheControl: cacheControl.getSerializedValue(), + .date: HTTPCacheControl.getDateHeaderValue() + ] - if let expiresValue = HTTPCacheControl.getExpiresValue(for: filePath) { - headers[.lastModified] = expiresValue - if let ifModifiedSince = request.headers[.ifModifiedSince], expiresValue == ifModifiedSince { - return HTTPResponse(statusCode: .notModified, - headers: headers) + if let expiresValue = HTTPCacheControl.getExpiresValue(for: filePath) { + headers[.lastModified] = expiresValue + if let ifModifiedSince = request.headers[.ifModifiedSince], expiresValue == ifModifiedSince { + return HTTPResponse(statusCode: .notModified, + headers: headers) + } } - } - if let eTagValue = HTTPCacheControl.getETagValue(for: filePath) { - headers[.eTag] = eTagValue - if let ifNoneMatch = request.headers[.ifNoneMatch], eTagValue == ifNoneMatch { - return HTTPResponse(statusCode: .notModified, - headers: headers) + if let eTagValue = HTTPCacheControl.getETagValue(for: filePath) { + headers[.eTag] = eTagValue + if let ifNoneMatch = request.headers[.ifNoneMatch], eTagValue == ifNoneMatch { + return HTTPResponse(statusCode: .notModified, + headers: headers) + } } - } - return HTTPResponse( - statusCode: .ok, - headers: headers, - body: data - ) + return try HTTPResponse( + statusCode: .ok, + headers: headers, + body: HTTPBodySequence(file: filePath) + ) + } catch { + return HTTPResponse(statusCode: .notFound) + } } func makeFileURL(for requestPath: String) -> URL? { diff --git a/FlyingFox/Tests/Handlers/DirectoryHTTPHandlerTests.swift b/FlyingFox/Tests/Handlers/DirectoryHTTPHandlerTests.swift index c81fee16..d3ab6ae5 100644 --- a/FlyingFox/Tests/Handlers/DirectoryHTTPHandlerTests.swift +++ b/FlyingFox/Tests/Handlers/DirectoryHTTPHandlerTests.swift @@ -72,6 +72,18 @@ struct DirectoryHTTPHandlerTests { ) } + @Test + func directoryHandler_streamsBody_fromFile() async throws { + let handler = DirectoryHTTPHandler(bundle: .module, subPath: "Stubs", serverPath: "server/path") + + let response = try await handler.handleRequest(.make(path: "server/path/fish.json")) + guard case .httpBody(let body) = response.payload else { + Issue.record("expected .httpBody payload") + return + } + #expect(body.storage.sequence is AsyncBufferedFileSequence) + } + @Test func directoryHandler_Returns404WhenFileDoesNotExist() async throws { let handler = DirectoryHTTPHandler.directory(for: .module, subPath: "Stubs", serverPath: "server/path") From 31e74afa1d0d40f4cece5c13cdc5e4993dd123ce Mon Sep 17 00:00:00 2001 From: Ian Gordon Date: Tue, 28 Apr 2026 12:48:31 -0400 Subject: [PATCH 2/2] Cover DirectoryHTTPHandler conditional-request paths Adds tests for Cache-Control/Date/Last-Modified/ETag header emission on 200 responses, plus 304 round-trips and 200 fallthrough for both If-Modified-Since and If-None-Match. Exercises lines in DirectoryHTTPHandler.handleRequest that previously had no coverage. Co-Authored-By: Claude Opus 4.7 --- .../Handlers/DirectoryHTTPHandlerTests.swift | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/FlyingFox/Tests/Handlers/DirectoryHTTPHandlerTests.swift b/FlyingFox/Tests/Handlers/DirectoryHTTPHandlerTests.swift index d3ab6ae5..17d08585 100644 --- a/FlyingFox/Tests/Handlers/DirectoryHTTPHandlerTests.swift +++ b/FlyingFox/Tests/Handlers/DirectoryHTTPHandlerTests.swift @@ -84,6 +84,70 @@ struct DirectoryHTTPHandlerTests { #expect(body.storage.sequence is AsyncBufferedFileSequence) } + @Test + func directoryHandler_setsCacheHeaders_on200() async throws { + let handler = DirectoryHTTPHandler(bundle: .module, subPath: "Stubs", serverPath: "server/path") + + let response = try await handler.handleRequest(.make(path: "server/path/fish.json")) + #expect(response.statusCode == .ok) + #expect(response.headers[.cacheControl]?.isEmpty == false) + #expect(response.headers[.date]?.isEmpty == false) + #expect(response.headers[.lastModified]?.isEmpty == false) + #expect(response.headers[.eTag]?.isEmpty == false) + } + + @Test + func directoryHandler_returns304_whenIfModifiedSinceMatches() async throws { + let handler = DirectoryHTTPHandler(bundle: .module, subPath: "Stubs", serverPath: "server/path") + + let initial = try await handler.handleRequest(.make(path: "server/path/fish.json")) + let lastModified = try #require(initial.headers[.lastModified]) + + let response = try await handler.handleRequest(.make( + path: "server/path/fish.json", + headers: [.ifModifiedSince: lastModified] + )) + #expect(response.statusCode == .notModified) + #expect(response.headers[.lastModified] == lastModified) + } + + @Test + func directoryHandler_returns200_whenIfModifiedSinceDoesNotMatch() async throws { + let handler = DirectoryHTTPHandler(bundle: .module, subPath: "Stubs", serverPath: "server/path") + + let response = try await handler.handleRequest(.make( + path: "server/path/fish.json", + headers: [.ifModifiedSince: "Mon, 01 Jan 1990 00:00:00 GMT"] + )) + #expect(response.statusCode == .ok) + } + + @Test + func directoryHandler_returns304_whenIfNoneMatchMatches() async throws { + let handler = DirectoryHTTPHandler(bundle: .module, subPath: "Stubs", serverPath: "server/path") + + let initial = try await handler.handleRequest(.make(path: "server/path/fish.json")) + let etag = try #require(initial.headers[.eTag]) + + let response = try await handler.handleRequest(.make( + path: "server/path/fish.json", + headers: [.ifNoneMatch: etag] + )) + #expect(response.statusCode == .notModified) + #expect(response.headers[.eTag] == etag) + } + + @Test + func directoryHandler_returns200_whenIfNoneMatchDoesNotMatch() async throws { + let handler = DirectoryHTTPHandler(bundle: .module, subPath: "Stubs", serverPath: "server/path") + + let response = try await handler.handleRequest(.make( + path: "server/path/fish.json", + headers: [.ifNoneMatch: "\"deadbeef-0\""] + )) + #expect(response.statusCode == .ok) + } + @Test func directoryHandler_Returns404WhenFileDoesNotExist() async throws { let handler = DirectoryHTTPHandler.directory(for: .module, subPath: "Stubs", serverPath: "server/path")