diff --git a/FlyingFox/Sources/Handlers/DirectoryHTTPHandler.swift b/FlyingFox/Sources/Handlers/DirectoryHTTPHandler.swift index 910cf59..fd35d5a 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 c81fee1..17d0858 100644 --- a/FlyingFox/Tests/Handlers/DirectoryHTTPHandlerTests.swift +++ b/FlyingFox/Tests/Handlers/DirectoryHTTPHandlerTests.swift @@ -72,6 +72,82 @@ 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_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")