@@ -182,7 +182,7 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
182
182
183
183
public func stream(
184
184
request: any HTTPRequestConvertible ,
185
- consume: @escaping ( Data , Double ? ) -> Void
185
+ consume: @escaping ( Data , Double ? ) -> HTTPResult < Void >
186
186
) async -> HTTPResult < HTTPResponse > {
187
187
await request. httpRequest ( )
188
188
. asyncFlatMap ( willStartRequest)
@@ -321,11 +321,7 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
321
321
typealias Continuation = CheckedContinuation < HTTPResult < HTTPResponse > , Never >
322
322
typealias ReceiveResponse = ( HTTPResponse ) -> Void
323
323
typealias ReceiveChallenge = ( URLAuthenticationChallenge ) async -> URLAuthenticationChallengeResponse
324
- typealias Consume = ( Data , Double ? ) -> Void
325
-
326
- enum TaskError : Error {
327
- case byteRangesNotSupported( url: HTTPURL )
328
- }
324
+ typealias Consume = ( Data , Double ? ) -> HTTPResult < Void >
329
325
330
326
private let request : HTTPRequest
331
327
fileprivate let task : URLSessionTask
@@ -339,13 +335,20 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
339
335
private enum State {
340
336
/// Waiting to start the task.
341
337
case initializing
338
+
342
339
/// Waiting for the HTTP response.
343
340
case start( continuation: Continuation )
344
- /// We received a success response, the data will be sent to `consume` progressively.
341
+
342
+ /// We received a success response, the data will be sent to
343
+ /// `consume` progressively.
345
344
case stream( continuation: Continuation , response: HTTPResponse , readBytes: Int64 )
346
- /// We received an error response, the data will be accumulated in `response.body` to make the final
347
- /// `HTTPError`. The body is needed for example when the response is an OPDS Authentication Document.
348
- case failure( continuation: Continuation , kind: HTTPError . Kind , cause: Error ? , response: HTTPResponse ? )
345
+
346
+ /// We received an error response, the data will be accumulated in
347
+ /// `response.body` if the error is an `HTTPError.errorResponse`, as
348
+ /// it could be needed for example when the response is an OPDS
349
+ /// Authentication Document.
350
+ case failure( continuation: Continuation , error: HTTPError )
351
+
349
352
/// The request is terminated.
350
353
case finished
351
354
@@ -357,7 +360,7 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
357
360
return continuation
358
361
case let . stream( continuation, _, _) :
359
362
return continuation
360
- case let . failure( continuation, _, _ , _ ) :
363
+ case let . failure( continuation, _) :
361
364
return continuation
362
365
}
363
366
}
@@ -394,14 +397,15 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
394
397
private func finish( ) {
395
398
switch state {
396
399
case let . start( continuation) :
397
- continuation. resume ( returning: . failure( HTTPError ( kind : . cancelled) ) )
400
+ continuation. resume ( returning: . failure( . cancelled) )
398
401
399
402
case let . stream( continuation, response, _) :
400
403
continuation. resume ( returning: . success( response) )
401
404
402
- case let . failure( continuation, kind, cause, response) :
403
- let error = HTTPError ( kind: kind, cause: cause, response: response)
404
- log ( . error, " \( request. method) \( request. url) failed with: \( error. localizedDescription) " )
405
+ case let . failure( continuation, error) :
406
+ var errorDescription = " "
407
+ dump ( error, to: & errorDescription)
408
+ log ( . error, " \( request. method) \( request. url) failed with: \n \( errorDescription) " )
405
409
continuation. resume ( returning: . failure( error) )
406
410
407
411
case . initializing, . finished:
@@ -427,34 +431,22 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
427
431
428
432
var response = HTTPResponse ( request: request, response: urlResponse, url: url)
429
433
430
- if let kind = HTTPError . Kind ( statusCode: response. statusCode) {
431
- state = . failure( continuation: continuation, kind: kind, cause: nil , response: response)
432
-
433
- // It was a HEAD request? We need to query the resource again to get the error body. The body is needed
434
- // for example when the response is an OPDS Authentication Document.
435
- if request. method == . head {
436
- var modifiedRequest = request
437
- modifiedRequest. method = . get
438
- session. dataTask ( with: modifiedRequest. urlRequest) { data, _, error in
439
- response. body = data
440
- self . state = . failure( continuation: continuation, kind: kind, cause: error, response: response)
441
- completionHandler ( . cancel)
442
- } . resume ( )
443
- return
444
- }
445
-
446
- } else {
447
- guard !request. hasHeader ( " Range " ) || response. acceptsByteRanges else {
448
- log ( . error, " Streaming ranges requires the remote HTTP server to support byte range requests: \( url) " )
449
- state = . failure( continuation: continuation, kind: . other, cause: TaskError . byteRangesNotSupported ( url: url) , response: response)
450
- completionHandler ( . cancel)
451
- return
452
- }
434
+ guard response. status. isSuccess else {
435
+ state = . failure( continuation: continuation, error: . errorResponse( response) )
436
+ completionHandler ( . allow)
437
+ return
438
+ }
453
439
454
- state = . stream( continuation: continuation, response: response, readBytes: 0 )
455
- receiveResponse ( response)
440
+ guard !request. hasHeader ( " Range " ) || response. acceptsByteRanges else {
441
+ log ( . error, " Streaming ranges requires the remote HTTP server to support byte range requests: \( url) " )
442
+ state = . failure( continuation: continuation, error: . rangeNotSupported)
443
+ completionHandler ( . cancel)
444
+ return
456
445
}
457
446
447
+ state = . stream( continuation: continuation, response: response, readBytes: 0 )
448
+ receiveResponse ( response)
449
+
458
450
completionHandler ( . allow)
459
451
}
460
452
@@ -469,14 +461,23 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
469
461
if let expectedBytes = response. contentLength {
470
462
progress = Double ( min ( readBytes, expectedBytes) ) / Double( expectedBytes)
471
463
}
472
- consume ( data, progress)
473
- state = . stream( continuation: continuation, response: response, readBytes: readBytes)
474
-
475
- case . failure( let continuation, let kind, let cause, var response) :
476
- var body = response? . body ?? Data ( )
477
- body. append ( data)
478
- response? . body = body
479
- state = . failure( continuation: continuation, kind: kind, cause: cause, response: response)
464
+
465
+ switch consume ( data, progress) {
466
+ case . success:
467
+ state = . stream( continuation: continuation, response: response, readBytes: readBytes)
468
+ case let . failure( error) :
469
+ state = . failure( continuation: continuation, error: error)
470
+ }
471
+
472
+ case . failure( let continuation, var error) :
473
+ if case var . errorResponse( response) = error {
474
+ var body = response. body ?? Data ( )
475
+ body. append ( data)
476
+ response. body = body
477
+ error = . errorResponse( response)
478
+ }
479
+
480
+ state = . failure( continuation: continuation, error: error)
480
481
}
481
482
}
482
483
@@ -485,7 +486,7 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
485
486
if case . failure = state {
486
487
// No-op, we don't want to overwrite the failure state in this case.
487
488
} else if let continuation = state. continuation {
488
- state = . failure( continuation: continuation, kind : HTTPError . Kind ( error: error) , cause : error , response : nil )
489
+ state = . failure( continuation: continuation, error : HTTPError ( error: error) )
489
490
} else {
490
491
state = . finished
491
492
}
@@ -511,6 +512,35 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
511
512
}
512
513
}
513
514
515
+ private extension HTTPError {
516
+ /// Maps a native `URLError` to `HTTPError`.
517
+ init ( error: Error ) {
518
+ switch error {
519
+ case let error as URLError :
520
+ switch error. code {
521
+ case . httpTooManyRedirects, . redirectToNonExistentLocation:
522
+ self = . redirection( error)
523
+ case . secureConnectionFailed, . clientCertificateRejected, . clientCertificateRequired, . appTransportSecurityRequiresSecureConnection, . userAuthenticationRequired:
524
+ self = . security( error)
525
+ case . badServerResponse, . zeroByteResource, . cannotDecodeContentData, . cannotDecodeRawData, . dataLengthExceedsMaximum:
526
+ self = . malformedResponse( error)
527
+ case . notConnectedToInternet, . networkConnectionLost:
528
+ self = . offline( error)
529
+ case . cannotConnectToHost, . cannotFindHost:
530
+ self = . unreachable( error)
531
+ case . timedOut:
532
+ self = . timeout( error)
533
+ case . cancelled, . userCancelledAuthentication:
534
+ self = . cancelled
535
+ default :
536
+ self = . other( error)
537
+ }
538
+ default :
539
+ self = . other( error)
540
+ }
541
+ }
542
+ }
543
+
514
544
private extension HTTPRequest {
515
545
var urlRequest : URLRequest {
516
546
var request = URLRequest ( url: url. url)
@@ -545,7 +575,7 @@ private extension HTTPResponse {
545
575
self . init (
546
576
request: request,
547
577
url: url,
548
- statusCode : response. statusCode,
578
+ status : HTTPStatus ( rawValue : response. statusCode) ,
549
579
headers: headers,
550
580
mediaType: response. mimeType. flatMap { MediaType ( $0) } ,
551
581
body: body
0 commit comments