Skip to content

Commit 00d3869

Browse files
Merge pull request #1279 from apollographql/fix/upload-multi-variable
Fix for multi-variable uploads
2 parents b028941 + 4dbde0c commit 00d3869

2 files changed

Lines changed: 111 additions & 12 deletions

File tree

Sources/Apollo/RequestCreator.swift

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,15 @@ extension RequestCreator {
107107

108108
// Make sure all fields for files are set to null, or the server won't look
109109
// for the files in the rest of the form data
110-
let fieldsForFiles = Set(files.map { $0.fieldName })
110+
let fieldsForFiles = Set(files.map { $0.fieldName }).sorted()
111111
var fields = requestBody(for: operation, sendOperationIdentifiers: sendOperationIdentifiers)
112112
var variables = fields["variables"] as? GraphQLMap ?? GraphQLMap()
113113
for fieldName in fieldsForFiles {
114114
if
115115
let value = variables[fieldName],
116116
let arrayValue = value as? [JSONEncodable] {
117-
let updatedArray: [JSONEncodable?] = arrayValue.map { _ in nil }
118-
variables.updateValue(updatedArray, forKey: fieldName)
117+
let arrayOfNils: [JSONEncodable?] = arrayValue.map { _ in nil }
118+
variables.updateValue(arrayOfNils, forKey: fieldName)
119119
} else {
120120
variables.updateValue(nil, forKey: fieldName)
121121
}
@@ -125,20 +125,33 @@ extension RequestCreator {
125125
let operationData = try serializationFormat.serialize(value: fields)
126126
formData.appendPart(data: operationData, name: "operations")
127127

128+
// If there are multiple files for the same field, make sure to include them with indexes for the field. If there are multiple files for different fields, just use the field name.
128129
var map = [String: [String]]()
129-
if files.count == 1 {
130-
let firstFile = files.first!
131-
map["0"] = ["variables.\(firstFile.fieldName)"]
132-
} else {
133-
for (index, file) in files.enumerated() {
134-
map["\(index)"] = ["variables.\(file.fieldName).\(index)"]
130+
var currentIndex = 0
131+
132+
var sortedFiles = [GraphQLFile]()
133+
for fieldName in fieldsForFiles {
134+
let filesForField = files.filter { $0.fieldName == fieldName }
135+
if filesForField.count == 1 {
136+
let firstFile = filesForField.first!
137+
map["\(currentIndex)"] = ["variables.\(firstFile.fieldName)"]
138+
sortedFiles.append(firstFile)
139+
currentIndex += 1
140+
} else {
141+
for (index, file) in filesForField.enumerated() {
142+
map["\(currentIndex)"] = ["variables.\(file.fieldName).\(index)"]
143+
sortedFiles.append(file)
144+
currentIndex += 1
145+
}
135146
}
136147
}
148+
149+
assert(sortedFiles.count == files.count, "Number of sorted files did not equal the number of incoming files - some field name has been left out.")
137150

138151
let mapData = try serializationFormat.serialize(value: map)
139152
formData.appendPart(data: mapData, name: "map")
140153

141-
for (index, file) in files.enumerated() {
154+
for (index, file) in sortedFiles.enumerated() {
142155
formData.appendPart(inputStream: try file.generateInputStream(),
143156
contentLength: file.contentLength,
144157
name: "\(index)",

Tests/ApolloTests/RequestCreatorTests.swift

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ class RequestCreatorTests: XCTestCase {
1414
private let customRequestCreator = TestCustomRequestCreator()
1515
private let apolloRequestCreator = ApolloRequestCreator()
1616

17-
18-
1917
private func checkString(_ string: String,
2018
includes expectedString: String,
2119
file: StaticString = #file,
@@ -289,6 +287,94 @@ Bravo file content.
289287
}
290288
}
291289

290+
func testMultipleFilesWithMultipleFieldsWithApolloRequestCreator() throws {
291+
let alphaFileURL = self.fileURLForFile(named: "a", extension: "txt")
292+
let alphaFile = try GraphQLFile(fieldName: "uploads",
293+
originalName: "a.txt",
294+
mimeType: "text/plain",
295+
fileURL: alphaFileURL)
296+
297+
let betaFileURL = self.fileURLForFile(named: "b", extension: "txt")
298+
let betaFile = try GraphQLFile(fieldName: "uploads",
299+
originalName: "b.txt",
300+
mimeType: "text/plain",
301+
fileURL: betaFileURL)
302+
303+
let charlieFileUrl = self.fileURLForFile(named: "c", extension: "txt")
304+
let charlieFile = try GraphQLFile(fieldName: "secondField",
305+
originalName: "c.txt",
306+
mimeType: "text/plain",
307+
fileURL: charlieFileUrl)
308+
309+
let data = try apolloRequestCreator.requestMultipartFormData(
310+
for: HeroNameQuery(),
311+
files: [alphaFile, betaFile, charlieFile],
312+
sendOperationIdentifiers: false,
313+
serializationFormat: JSONSerializationFormat.self,
314+
manualBoundary: "TEST.BOUNDARY"
315+
)
316+
317+
let stringToCompare = try self.string(from: data)
318+
319+
if JSONSerialization.dataCanBeSorted() {
320+
let expectedString = """
321+
--TEST.BOUNDARY
322+
Content-Disposition: form-data; name="operations"
323+
324+
{"operationName":"HeroName","query":"query HeroName($episode: Episode) {\\n hero(episode: $episode) {\\n __typename\\n name\\n }\\n}","variables":{"episode":null,\"secondField\":null,\"uploads\":null}}
325+
--TEST.BOUNDARY
326+
Content-Disposition: form-data; name="map"
327+
328+
{"0":["variables.secondField"],"1":["variables.uploads.0"],"2":["variables.uploads.1"]}
329+
--TEST.BOUNDARY
330+
Content-Disposition: form-data; name="0"; filename="c.txt"
331+
Content-Type: text/plain
332+
333+
Charlie file content.
334+
335+
--TEST.BOUNDARY
336+
Content-Disposition: form-data; name="1"; filename="a.txt"
337+
Content-Type: text/plain
338+
339+
Alpha file content.
340+
341+
--TEST.BOUNDARY
342+
Content-Disposition: form-data; name="2"; filename="b.txt"
343+
Content-Type: text/plain
344+
345+
Bravo file content.
346+
347+
--TEST.BOUNDARY--
348+
"""
349+
XCTAssertEqual(stringToCompare, expectedString)
350+
} else {
351+
// Query and operation parameters may be in weird order, so let's at least check that the files got encoded properly.
352+
let endString = """
353+
--TEST.BOUNDARY
354+
Content-Disposition: form-data; name="0"; filename="c.txt"
355+
Content-Type: text/plain
356+
357+
Charlie file content.
358+
359+
--TEST.BOUNDARY
360+
Content-Disposition: form-data; name="1"; filename="a.txt"
361+
Content-Type: text/plain
362+
363+
Alpha file content.
364+
365+
--TEST.BOUNDARY
366+
Content-Disposition: form-data; name="2"; filename="b.txt"
367+
Content-Type: text/plain
368+
369+
Bravo file content.
370+
371+
--TEST.BOUNDARY--
372+
"""
373+
self.checkString(stringToCompare, includes: endString)
374+
}
375+
}
376+
377+
292378
func testRequestBodyWithApolloRequestCreator() {
293379
let query = HeroNameQuery()
294380
let req = apolloRequestCreator.requestBody(for: query, sendOperationIdentifiers: false)

0 commit comments

Comments
 (0)