@@ -182,7 +182,7 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
182182
183183 public func stream(
184184 request: any HTTPRequestConvertible ,
185- consume: @escaping ( Data , Double ? ) -> Void
185+ consume: @escaping ( Data , Double ? ) -> HTTPResult < Void >
186186 ) async -> HTTPResult < HTTPResponse > {
187187 await request. httpRequest ( )
188188 . asyncFlatMap ( willStartRequest)
@@ -321,11 +321,7 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
321321 typealias Continuation = CheckedContinuation < HTTPResult < HTTPResponse > , Never >
322322 typealias ReceiveResponse = ( HTTPResponse ) -> Void
323323 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 >
329325
330326 private let request : HTTPRequest
331327 fileprivate let task : URLSessionTask
@@ -339,13 +335,20 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
339335 private enum State {
340336 /// Waiting to start the task.
341337 case initializing
338+
342339 /// Waiting for the HTTP response.
343340 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.
345344 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+
349352 /// The request is terminated.
350353 case finished
351354
@@ -357,7 +360,7 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
357360 return continuation
358361 case let . stream( continuation, _, _) :
359362 return continuation
360- case let . failure( continuation, _, _ , _ ) :
363+ case let . failure( continuation, _) :
361364 return continuation
362365 }
363366 }
@@ -394,14 +397,15 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
394397 private func finish( ) {
395398 switch state {
396399 case let . start( continuation) :
397- continuation. resume ( returning: . failure( HTTPError ( kind : . cancelled) ) )
400+ continuation. resume ( returning: . failure( . cancelled) )
398401
399402 case let . stream( continuation, response, _) :
400403 continuation. resume ( returning: . success( response) )
401404
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) " )
405409 continuation. resume ( returning: . failure( error) )
406410
407411 case . initializing, . finished:
@@ -427,34 +431,22 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
427431
428432 var response = HTTPResponse ( request: request, response: urlResponse, url: url)
429433
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+ }
453439
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
456445 }
457446
447+ state = . stream( continuation: continuation, response: response, readBytes: 0 )
448+ receiveResponse ( response)
449+
458450 completionHandler ( . allow)
459451 }
460452
@@ -469,14 +461,23 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
469461 if let expectedBytes = response. contentLength {
470462 progress = Double ( min ( readBytes, expectedBytes) ) / Double( expectedBytes)
471463 }
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)
480481 }
481482 }
482483
@@ -485,7 +486,7 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
485486 if case . failure = state {
486487 // No-op, we don't want to overwrite the failure state in this case.
487488 } 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) )
489490 } else {
490491 state = . finished
491492 }
@@ -511,6 +512,35 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
511512 }
512513}
513514
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+
514544private extension HTTPRequest {
515545 var urlRequest : URLRequest {
516546 var request = URLRequest ( url: url. url)
@@ -545,7 +575,7 @@ private extension HTTPResponse {
545575 self . init (
546576 request: request,
547577 url: url,
548- statusCode : response. statusCode,
578+ status : HTTPStatus ( rawValue : response. statusCode) ,
549579 headers: headers,
550580 mediaType: response. mimeType. flatMap { MediaType ( $0) } ,
551581 body: body
0 commit comments