Skip to content

Commit e3aebe7

Browse files
gh-action-runnergh-action-runner
authored andcommitted
Squashed 'apollo-ios/' changes from 7122421c..bc5a0cdf
bc5a0cdf fix: Multipart delimiter boundary parsing (#502) git-subtree-dir: apollo-ios git-subtree-split: bc5a0cdfd11388824aace092701cbdd19ac1c575
1 parent f7b9725 commit e3aebe7

File tree

2 files changed

+57
-12
lines changed

2 files changed

+57
-12
lines changed

Sources/Apollo/MultipartResponseParsingInterceptor.swift

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,14 @@ public struct MultipartResponseParsingInterceptor: ApolloInterceptor {
8585
return
8686
}
8787

88-
for chunk in dataString.components(separatedBy: "--\(boundary)") {
89-
if chunk.isEmpty || chunk.isBoundaryMarker { continue }
88+
// Parsing Notes:
89+
//
90+
// Multipart messages arriving here may consist of more than one chunk, but they are always
91+
// expected to be complete chunks. Downstream protocol specification parsers are only built
92+
// to handle the protocol specific message formats, i.e.: data between the multipart delimiter.
93+
let boundaryDelimiter = Self.boundaryDelimiter(with: boundary)
94+
for chunk in dataString.components(separatedBy: boundaryDelimiter) {
95+
if chunk.isEmpty || chunk.isDashBoundaryPrefix || chunk.isMultipartNewLine { continue }
9096

9197
switch parser.parse(chunk) {
9298
case let .success(data):
@@ -119,6 +125,8 @@ public struct MultipartResponseParsingInterceptor: ApolloInterceptor {
119125
}
120126
}
121127

128+
// MARK: Specification Parser Protocol
129+
122130
/// A protocol that multipart response parsers must conform to in order to be added to the list of
123131
/// available response specification parsers.
124132
protocol MultipartResponseSpecificationParser {
@@ -140,6 +148,38 @@ extension MultipartResponseSpecificationParser {
140148
static var dataLineSeparator: StaticString { "\r\n\r\n" }
141149
}
142150

143-
fileprivate extension String {
144-
var isBoundaryMarker: Bool { self == "--" }
151+
// MARK: Helpers
152+
153+
extension MultipartResponseParsingInterceptor {
154+
static func boundaryDelimiter(with boundary: String) -> String {
155+
"\r\n--\(boundary)"
156+
}
157+
158+
static func closeBoundaryDelimiter(with boundary: String) -> String {
159+
boundaryDelimiter(with: boundary) + "--"
160+
}
161+
}
162+
163+
extension String {
164+
fileprivate var isDashBoundaryPrefix: Bool { self == "--" }
165+
fileprivate var isMultipartNewLine: Bool { self == "\r\n" }
166+
167+
/// Returns the range of a complete multipart chunk.
168+
func multipartRange(using boundary: String) -> String.Index? {
169+
// The end boundary marker indicates that no further chunks will follow so if this delimiter
170+
// if found then include the delimiter in the index. Search for this first.
171+
let closeBoundaryDelimiter = MultipartResponseParsingInterceptor.closeBoundaryDelimiter(with: boundary)
172+
if let endIndex = range(of: closeBoundaryDelimiter, options: .backwards)?.upperBound {
173+
return endIndex
174+
}
175+
176+
// A chunk boundary indicates there may still be more chunks to follow so the index need not
177+
// include the chunk boundary in the index.
178+
let boundaryDelimiter = MultipartResponseParsingInterceptor.boundaryDelimiter(with: boundary)
179+
if let chunkIndex = range(of: boundaryDelimiter, options: .backwards)?.lowerBound {
180+
return chunkIndex
181+
}
182+
183+
return nil
184+
}
145185
}

Sources/Apollo/URLSessionClient.swift

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -292,23 +292,28 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
292292
taskData.append(additionalData: data)
293293

294294
if let httpResponse = dataTask.response as? HTTPURLResponse, httpResponse.isMultipart {
295-
let multipartHeaderComponents = httpResponse.multipartHeaderComponents
296-
guard let boundaryString = multipartHeaderComponents.boundary else {
295+
guard let boundary = httpResponse.multipartHeaderComponents.boundary else {
297296
taskData.completionBlock(.failure(URLSessionClientError.missingMultipartBoundary))
298297
return
299298
}
300299

301-
let boundaryMarker = "--\(boundaryString)"
300+
// Parsing Notes:
301+
//
302+
// Multipart messages are parsed here only to look for complete chunks to pass on to the downstream
303+
// parsers. Any leftover data beyond a delimited chunk is held back for more data to arrive.
304+
//
305+
// Do not return `.failure` here simply because there was no boundary delimiter found; the
306+
// data may still be arriving. If the request ends without more data arriving it will get handled
307+
// in urlSession(_:task:didCompleteWithError:).
302308
guard
303-
let dataString = String(data: taskData.data, encoding: .utf8)?.trimmingCharacters(in: .newlines),
304-
let lastBoundaryIndex = dataString.range(of: boundaryMarker, options: .backwards)?.upperBound,
305-
let boundaryData = dataString.prefix(upTo: lastBoundaryIndex).data(using: .utf8)
309+
let dataString = String(data: taskData.data, encoding: .utf8),
310+
let lastBoundaryDelimiterIndex = dataString.multipartRange(using: boundary),
311+
let boundaryData = dataString.prefix(upTo: lastBoundaryDelimiterIndex).data(using: .utf8)
306312
else {
307-
taskData.completionBlock(.failure(URLSessionClientError.cannotParseBoundaryData))
308313
return
309314
}
310315

311-
let remainingData = dataString.suffix(from: lastBoundaryIndex).data(using: .utf8)
316+
let remainingData = dataString.suffix(from: lastBoundaryDelimiterIndex).data(using: .utf8)
312317
taskData.reset(data: remainingData)
313318

314319
if let rawCompletion = taskData.rawCompletion {

0 commit comments

Comments
 (0)