diff --git a/Sources/Hummingbird/Files/FileIO.swift b/Sources/Hummingbird/Files/FileIO.swift index 3edbafe9..eafdb368 100644 --- a/Sources/Hummingbird/Files/FileIO.swift +++ b/Sources/Hummingbird/Files/FileIO.swift @@ -39,6 +39,7 @@ public struct FileIO: Sendable { public func loadFile(path: String, context: some RequestContext, chunkLength: Int = NonBlockingFileIO.defaultChunkSize) async throws -> ResponseBody { do { let stat = try await fileIO.lstat(path: path) + guard stat.st_size > 0 else { return .init() } return self.readFile(path: path, range: 0...numericCast(stat.st_size - 1), context: context, chunkLength: chunkLength) } catch { throw HTTPError(.notFound) @@ -58,6 +59,7 @@ public struct FileIO: Sendable { public func loadFile(path: String, range: ClosedRange, context: some RequestContext, chunkLength: Int = NonBlockingFileIO.defaultChunkSize) async throws -> ResponseBody { do { let stat = try await fileIO.lstat(path: path) + guard stat.st_size > 0 else { return .init() } let fileRange: ClosedRange = 0...numericCast(stat.st_size - 1) let range = range.clamped(to: fileRange) return self.readFile(path: path, range: range, context: context, chunkLength: chunkLength) diff --git a/Tests/HummingbirdTests/FileIOTests.swift b/Tests/HummingbirdTests/FileIOTests.swift index 833ac59a..e7079684 100644 --- a/Tests/HummingbirdTests/FileIOTests.swift +++ b/Tests/HummingbirdTests/FileIOTests.swift @@ -90,4 +90,46 @@ class FileIOTests: XCTestCase { XCTAssertEqual(Data(buffer: buffer), data) } } + + func testReadEmptyFile() async throws { + let router = Router() + router.get("empty.txt") { _, context -> Response in + let fileIO = FileIO(threadPool: .singleton) + let body = try await fileIO.loadFile(path: "empty.txt", context: context) + return .init(status: .ok, headers: [:], body: body) + } + let data = Data() + let fileURL = URL(fileURLWithPath: "empty.txt") + XCTAssertNoThrow(try data.write(to: fileURL)) + defer { XCTAssertNoThrow(try FileManager.default.removeItem(at: fileURL)) } + + let app = Application(responder: router.buildResponder()) + + try await app.test(.router) { client in + try await client.execute(uri: "/empty.txt", method: .get) { response in + XCTAssertEqual(response.status, .ok) + } + } + } + + func testReadEmptyFilePart() async throws { + let router = Router() + router.get("empty.txt") { _, context -> Response in + let fileIO = FileIO(threadPool: .singleton) + let body = try await fileIO.loadFile(path: "empty.txt", range: 0...10, context: context) + return .init(status: .ok, headers: [:], body: body) + } + let data = Data() + let fileURL = URL(fileURLWithPath: "empty.txt") + XCTAssertNoThrow(try data.write(to: fileURL)) + defer { XCTAssertNoThrow(try FileManager.default.removeItem(at: fileURL)) } + + let app = Application(responder: router.buildResponder()) + + try await app.test(.router) { client in + try await client.execute(uri: "/empty.txt", method: .get) { response in + XCTAssertEqual(response.status, .ok) + } + } + } }