Skip to content
This repository was archived by the owner on Sep 15, 2025. It is now read-only.

Commit 5671164

Browse files
authored
Remove temp file once HTTP request completes (#740)
2 parents b30f987 + 753243b commit 5671164

File tree

3 files changed

+86
-5
lines changed

3 files changed

+86
-5
lines changed

WordPressKit/HTTPClient.swift

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ extension URLSession {
109109

110110
private func task(
111111
for builder: HTTPRequestBuilder,
112-
completion: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void
112+
completion originalCompletion: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void
113113
) throws -> URLSessionTask {
114114
var request = try builder.build(encodeBody: false)
115115

@@ -123,6 +123,7 @@ extension URLSession {
123123
let task: URLSessionTask
124124
let body = try builder.encodeMultipartForm(request: &request, forceWriteToFile: isBackgroundSession)
125125
?? builder.encodeXMLRPC(request: &request, forceWriteToFile: isBackgroundSession)
126+
var completion = originalCompletion
126127
if let body {
127128
// Use special `URLSession.uploadTask` API for multipart POST requests.
128129
task = body.map(
@@ -133,11 +134,17 @@ extension URLSession {
133134
return uploadTask(with: request, from: $0, completionHandler: completion)
134135
}
135136
},
136-
right: {
137+
right: { tempFileURL in
138+
// Remove the temp file, which contains request body, once the HTTP request completes.
139+
completion = { data, response, error in
140+
try? FileManager.default.removeItem(at: tempFileURL)
141+
originalCompletion(data, response, error)
142+
}
143+
137144
if callCompletionFromDelegate {
138-
return uploadTask(with: request, fromFile: $0)
145+
return uploadTask(with: request, fromFile: tempFileURL)
139146
} else {
140-
return uploadTask(with: request, fromFile: $0, completionHandler: completion)
147+
return uploadTask(with: request, fromFile: tempFileURL, completionHandler: completion)
141148
}
142149
}
143150
)

WordPressKit/HTTPRequestBuilder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ final class HTTPRequestBuilder {
215215
request.setValue("text/xml", forHTTPHeaderField: "Content-Type")
216216
let encoder = WPXMLRPCEncoder(method: xmlrpcRequest.method, andParameters: xmlrpcRequest.parameters)
217217
if forceWriteToFile {
218-
let fileName = "\(ProcessInfo.processInfo.globallyUniqueString)_file.xmlrpc"
218+
let fileName = "\(UUID().uuidString).xmlrpc"
219219
let fileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName)
220220
try encoder.encode(toFile: fileURL.path)
221221

WordPressKitTests/Utilities/URLSessionHelperTests.swift

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,80 @@ class URLSessionHelperTests: XCTestCase {
256256
)
257257
}
258258

259+
func testTempFileRemovedAfterMultipartUpload() async throws {
260+
stub(condition: isPath("/upload")) { _ in
261+
HTTPStubsResponse(data: "success".data(using: .utf8)!, statusCode: 200, headers: nil)
262+
}
263+
264+
// Create a large file which will be uploaded
265+
let file = try self.createLargeFile(megaBytes: 100)
266+
defer {
267+
try? FileManager.default.removeItem(at: file)
268+
}
269+
270+
// Capture a list of files in temp dirs, before calling the upload function.
271+
let tempFilesBeforeUpload = existingTempFiles()
272+
273+
// Perform upload HTTP request
274+
let builder = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org/upload")!)
275+
.method(.post)
276+
.body(form: [MultipartFormField(fileAtPath: file.path, name: "file", filename: "file.txt", mimeType: "text/plain")])
277+
let _ = await session.perform(request: builder, errorType: TestError.self)
278+
279+
// Capture a list of files in the temp dirs, after calling the upload function.
280+
let tempFilesAfterUpload = existingTempFiles()
281+
282+
// There should be no new files after the HTTP request returns. This assertion relies on an implementation detail
283+
// where the multipart form content is put into a file in temp dirs.
284+
let newFiles = tempFilesAfterUpload.subtracting(tempFilesBeforeUpload)
285+
XCTAssertEqual(newFiles.count, 0)
286+
}
287+
288+
func testTempFileRemovedAfterMultipartUploadError() async throws {
289+
stub(condition: isPath("/upload")) { _ in
290+
HTTPStubsResponse(error: URLError(.networkConnectionLost))
291+
}
292+
293+
// Create a large file which will be uploaded
294+
let file = try self.createLargeFile(megaBytes: 100)
295+
defer {
296+
try? FileManager.default.removeItem(at: file)
297+
}
298+
299+
// Capture a list of files in temp dirs, before calling the upload function.
300+
let tempFilesBeforeUpload = existingTempFiles()
301+
302+
// Perform upload HTTP request
303+
let builder = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org/upload")!)
304+
.method(.post)
305+
.body(form: [MultipartFormField(fileAtPath: file.path, name: "file", filename: "file.txt", mimeType: "text/plain")])
306+
let _ = await session.perform(request: builder, errorType: TestError.self)
307+
308+
// Capture a list of files in the temp dirs, after calling the upload function.
309+
let tempFilesAfterUpload = existingTempFiles()
310+
311+
// There should be no new files after the HTTP request returns. This assertion relies on an implementation detail
312+
// where the multipart form content is put into a file in temp dirs.
313+
let newFiles = tempFilesAfterUpload.subtracting(tempFilesBeforeUpload)
314+
XCTAssertEqual(newFiles.count, 0)
315+
}
316+
317+
private func existingTempFiles() -> Set<String> {
318+
let fm = FileManager.default
319+
let enumerators = [
320+
fm.enumerator(atPath: NSTemporaryDirectory()),
321+
fm.enumerator(atPath: fm.temporaryDirectory.path)
322+
].compactMap { $0 }
323+
324+
var result: Set<String> = []
325+
for enumerator in enumerators {
326+
while let file = enumerator.nextObject() as? String {
327+
result.insert(file)
328+
}
329+
}
330+
return result
331+
}
332+
259333
private func createLargeFile(megaBytes: Int) throws -> URL {
260334
let fileManager = FileManager.default
261335
let file = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)

0 commit comments

Comments
 (0)