Skip to content

Commit 9acd1ba

Browse files
committed
Fix: URLSessionHTTPClient only move file if response code is within OK range
1 parent c26009f commit 9acd1ba

File tree

2 files changed

+76
-2
lines changed

2 files changed

+76
-2
lines changed

Sources/Basics/HTTPClient/URLSessionHTTPClient.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,14 @@ private final class DownloadTaskManager: NSObject, URLSessionDownloadDelegate {
307307
do {
308308
let path = try AbsolutePath(validating: location.path)
309309

310+
// Check if we have a bad response code - if so, delete the temporary file instead of moving it
311+
if let response = downloadTask.response as? HTTPURLResponse,
312+
response.statusCode < 200 || response.statusCode >= 300 {
313+
// Delete the temporary file for bad response codes
314+
try task.fileSystem.removeFileTree(path)
315+
return
316+
}
317+
310318
// Always using synchronous `localFileSystem` here since `URLSession` requires temporary `location`
311319
// to be moved from synchronously. Otherwise the file will be immediately cleaned up after returning
312320
// from this delegate method.

Tests/BasicsTests/URLSessionHTTPClientTests.swift

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,8 @@ final class URLSessionHTTPClientTest: XCTestCase {
494494
let completionExpectation = XCTestExpectation(description: "completion")
495495

496496
let url = URL("https://downloader-tests.com/testServerError.zip")
497-
var request = LegacyHTTPClient.Request.download(url: url, fileSystem: localFileSystem, destination: temporaryDirectory.appending("download"))
497+
let destination = temporaryDirectory.appending("download")
498+
var request = LegacyHTTPClient.Request.download(url: url, fileSystem: localFileSystem, destination: destination)
498499
request.options.validResponseCodes = [200]
499500

500501
httpClient.execute(
@@ -508,6 +509,8 @@ final class URLSessionHTTPClientTest: XCTestCase {
508509
XCTFail("unexpected success")
509510
case .failure(let error):
510511
XCTAssertEqual(error as? HTTPClientError, HTTPClientError.badResponseStatusCode(500))
512+
// Verify that no file was created at the destination for bad response codes
513+
XCTAssertFalse(localFileSystem.exists(destination), "Destination file should not exist for bad response codes")
511514
}
512515
completionExpectation.fulfill()
513516
}
@@ -998,13 +1001,14 @@ final class URLSessionHTTPClientTest: XCTestCase {
9981001

9991002
try await testWithTemporaryDirectory { temporaryDirectory in
10001003
let url = URL("https://async-downloader-tests.com/testServerError.zip")
1004+
let destination = temporaryDirectory.appending("download")
10011005
var options = HTTPClientRequest.Options()
10021006
options.validResponseCodes = [200]
10031007
let request = HTTPClient.Request.download(
10041008
url: url,
10051009
options: options,
10061010
fileSystem: localFileSystem,
1007-
destination: temporaryDirectory.appending("download")
1011+
destination: destination
10081012
)
10091013

10101014
MockURLProtocol.onRequest(request) { request in
@@ -1022,6 +1026,68 @@ final class URLSessionHTTPClientTest: XCTestCase {
10221026
XCTFail("unexpected success")
10231027
} catch {
10241028
XCTAssertEqual(error as? HTTPClientError, HTTPClientError.badResponseStatusCode(500))
1029+
// Verify that no file was created at the destination for bad response codes
1030+
XCTAssertFalse(localFileSystem.exists(destination), "Destination file should not exist for bad response codes")
1031+
}
1032+
}
1033+
}
1034+
1035+
func testAsyncDownloadServerErrorAndResponseBody() async throws {
1036+
#if !os(macOS)
1037+
// URLSession Download tests can only run on macOS
1038+
// as re-libs-foundation's URLSessionTask implementation which expects the temporaryFileURL property to be on the request.
1039+
// and there is no way to set it in a mock
1040+
// https://github.com/apple/swift-corelibs-foundation/pull/2593 tries to address the latter part
1041+
try XCTSkipIf(true, "test is only supported on macOS")
1042+
#endif
1043+
let configuration = URLSessionConfiguration.default
1044+
configuration.protocolClasses = [MockURLProtocol.self]
1045+
let urlSession = URLSessionHTTPClient(configuration: configuration)
1046+
let httpClient = HTTPClient(implementation: urlSession.execute)
1047+
1048+
try await testWithTemporaryDirectory { temporaryDirectory in
1049+
let url = URL("https://async-downloader-tests.com/testServerErrorWithBody.zip")
1050+
let destination = temporaryDirectory.appending("download")
1051+
var options = HTTPClientRequest.Options()
1052+
options.validResponseCodes = [200]
1053+
let request = HTTPClient.Request.download(
1054+
url: url,
1055+
options: options,
1056+
fileSystem: localFileSystem,
1057+
destination: destination
1058+
)
1059+
1060+
// Create an error response body (e.g., JSON error message)
1061+
let errorJson = Data("""
1062+
{
1063+
"errors" : [ {
1064+
"status" : 500,
1065+
"message" : "Internal Server Error"
1066+
} ]
1067+
}
1068+
""".utf8)
1069+
1070+
MockURLProtocol.onRequest(request) { request in
1071+
MockURLProtocol.sendResponse(statusCode: 500, headers: ["Content-Type": "application/json", "Content-Length": "\(errorJson.count)"], for: request)
1072+
// Send error response body - this should be downloaded to temp file but then deleted
1073+
MockURLProtocol.sendData(errorJson, for: request)
1074+
MockURLProtocol.sendCompletion(for: request)
1075+
}
1076+
1077+
do {
1078+
_ = try await httpClient.execute(
1079+
request,
1080+
progress: { _, _ in
1081+
// Progress may be called as data is downloaded, even for error responses
1082+
// This is expected behavior
1083+
}
1084+
)
1085+
XCTFail("unexpected success")
1086+
} catch {
1087+
XCTAssertEqual(error as? HTTPClientError, HTTPClientError.badResponseStatusCode(500))
1088+
// Verify that no file was created at the destination for bad response codes
1089+
// even though a response body was sent
1090+
XCTAssertFalse(localFileSystem.exists(destination), "Destination file should not exist for bad response codes, even when response body is present")
10251091
}
10261092
}
10271093
}

0 commit comments

Comments
 (0)