Skip to content

Commit f29e57c

Browse files
sichanyooSichan Yoo
andauthored
fix: Fix CRT HTTP client continuation bug (#711)
* Fix continuation bug by using DispatchQueue and boolean flag. * Make CRT HTTP client default for macOS & reset continuatino boolean flag at start of each request. * Swiftlint. * Make continuation boolean flag contained within each independent request. * Swiftlint. * Swiftlint. * Address PR comments & use DispatchQueue to sync flag changes. * Remove dispatch queue & actor. Instead, just use a class wrapper for boolean flag. * Wrap continuation itself instead of using boolean flag. * Swiftlint. * Refactor to reduce nested Task & revert client compiler directive to make macOS use URLSession. * Resolve PR comments. --------- Co-authored-by: Sichan Yoo <[email protected]>
1 parent c9caeb7 commit f29e57c

File tree

1 file changed

+48
-21
lines changed

1 file changed

+48
-21
lines changed

Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,11 @@ public class CRTClientEngine: HTTPClient {
170170
self.logger.debug("Using HTTP/1.1 connection")
171171
let crtRequest = try request.toHttpRequest()
172172
return try await withCheckedThrowingContinuation { (continuation: StreamContinuation) in
173-
let requestOptions = makeHttpRequestStreamOptions(request: crtRequest,
174-
continuation: continuation)
173+
let wrappedContinuation = ContinuationWrapper(continuation)
174+
let requestOptions = makeHttpRequestStreamOptions(
175+
request: crtRequest,
176+
continuation: wrappedContinuation
177+
)
175178
do {
176179
let stream = try connection.makeRequest(requestOptions: requestOptions)
177180
try stream.activate()
@@ -217,28 +220,34 @@ public class CRTClientEngine: HTTPClient {
217220
}
218221
}
219222
} catch {
220-
continuation.resume(throwing: error)
223+
logger.error(error.localizedDescription)
224+
wrappedContinuation.safeResume(error: error)
221225
}
222226
}
223227
}
224228
} catch {
225-
continuation.resume(throwing: error)
229+
logger.error(error.localizedDescription)
230+
wrappedContinuation.safeResume(error: error)
226231
}
227232
}
228233
case .version_2:
229234
self.logger.debug("Using HTTP/2 connection")
230235
let crtRequest = try request.toHttp2Request()
231236
return try await withCheckedThrowingContinuation { (continuation: StreamContinuation) in
232-
let requestOptions = makeHttpRequestStreamOptions(request: crtRequest,
233-
continuation: continuation,
234-
http2ManualDataWrites: true)
237+
let wrappedContinuation = ContinuationWrapper(continuation)
238+
let requestOptions = makeHttpRequestStreamOptions(
239+
request: crtRequest,
240+
continuation: wrappedContinuation,
241+
http2ManualDataWrites: true
242+
)
235243
let stream: HTTP2Stream
236244
do {
237245
// swiftlint:disable:next force_cast
238246
stream = try connection.makeRequest(requestOptions: requestOptions) as! HTTP2Stream
239247
try stream.activate()
240248
} catch {
241-
continuation.resume(throwing: error)
249+
logger.error(error.localizedDescription)
250+
wrappedContinuation.safeResume(error: error)
242251
return
243252
}
244253

@@ -267,29 +276,29 @@ public class CRTClientEngine: HTTPClient {
267276
let crtRequest = try request.toHttp2Request()
268277

269278
return try await withCheckedThrowingContinuation { (continuation: StreamContinuation) in
279+
let wrappedContinuation = ContinuationWrapper(continuation)
270280
let requestOptions = makeHttpRequestStreamOptions(
271281
request: crtRequest,
272-
continuation: continuation,
282+
continuation: wrappedContinuation,
273283
http2ManualDataWrites: true
274284
)
275-
Task {
285+
Task { [logger] in
276286
let stream: HTTP2Stream
277287
do {
278288
stream = try await connectionMgr.acquireStream(requestOptions: requestOptions)
279289
} catch {
280-
continuation.resume(throwing: error)
290+
logger.error(error.localizedDescription)
291+
wrappedContinuation.safeResume(error: error)
281292
return
282293
}
283294

284295
// At this point, continuation is resumed when the initial headers are received
285296
// it is now safe to write the body
286297
// writing is done in a separate task to avoid blocking the continuation
287-
Task { [logger] in
288-
do {
289-
try await stream.write(body: request.body)
290-
} catch {
291-
logger.error(error.localizedDescription)
292-
}
298+
do {
299+
try await stream.write(body: request.body)
300+
} catch {
301+
logger.error(error.localizedDescription)
293302
}
294303
}
295304
}
@@ -298,7 +307,7 @@ public class CRTClientEngine: HTTPClient {
298307
/// Creates a `HTTPRequestOptions` object that can be used to make a HTTP request
299308
/// - Parameters:
300309
/// - request: The `HTTPRequestBase` object that contains the request information
301-
/// - continuation: The continuation that will be resumed when the request is complete
310+
/// - continuation: The wrapped continuation that will be resumed when the request is complete
302311
/// - http2ManualDataWrites: Whether or not the request is using HTTP/2 manual data writes, defaults to `false`
303312
/// If set to false, HTTP/2 manual data writes will be disabled and result in a runtime error on writing on the
304313
/// HTTP/2 stream
@@ -308,7 +317,7 @@ public class CRTClientEngine: HTTPClient {
308317
/// - Returns: A `HTTPRequestOptions` object that can be used to make a HTTP request
309318
private func makeHttpRequestStreamOptions(
310319
request: HTTPRequestBase,
311-
continuation: StreamContinuation,
320+
continuation: ContinuationWrapper,
312321
http2ManualDataWrites: Bool = false
313322
) -> HTTPRequestOptions {
314323
let response = HttpResponse()
@@ -330,7 +339,7 @@ public class CRTClientEngine: HTTPClient {
330339
// resume the continuation as soon as we have all the initial headers
331340
// this allows callers to start reading the response as it comes in
332341
// instead of waiting for the entire response to be received
333-
continuation.resume(returning: response)
342+
continuation.safeResume(response: response)
334343
} onIncomingBody: { bodyChunk in
335344
self.logger.debug("Body chunk received")
336345
do {
@@ -348,7 +357,7 @@ public class CRTClientEngine: HTTPClient {
348357
response.statusCode = makeStatusCode(statusCode)
349358
case .failure(let error):
350359
self.logger.error("Response encountered an error: \(error)")
351-
continuation.resume(throwing: error)
360+
continuation.safeResume(error: error)
352361
}
353362

354363
// closing the stream is required to signal to the caller that the response is complete
@@ -361,4 +370,22 @@ public class CRTClientEngine: HTTPClient {
361370
response.body = .stream(stream)
362371
return requestOptions
363372
}
373+
374+
class ContinuationWrapper {
375+
private var continuation: StreamContinuation?
376+
377+
public init(_ continuation: StreamContinuation) {
378+
self.continuation = continuation
379+
}
380+
381+
public func safeResume(response: HttpResponse) {
382+
continuation?.resume(returning: response)
383+
self.continuation = nil
384+
}
385+
386+
public func safeResume(error: Error) {
387+
continuation?.resume(throwing: error)
388+
self.continuation = nil
389+
}
390+
}
364391
}

0 commit comments

Comments
 (0)