Sources/SPTDataLoader/SPTDataLoaderResponse.m (148 lines of code) (raw):

/* Copyright Spotify AB. SPDX-License-Identifier: Apache-2.0 */ #import <SPTDataLoader/SPTDataLoaderResponse.h> #import "SPTDataLoaderResponse+Private.h" NS_ASSUME_NONNULL_BEGIN NSString * const SPTDataLoaderResponseErrorDomain = @"com.sptdataloaderresponse.error"; static NSString * const SPTDataLoaderResponseHeaderRetryAfter = @"Retry-After"; @interface SPTDataLoaderResponse () @property (nonatomic, strong, readwrite) NSDictionary<NSString *, NSString *> *responseHeaders; @property (nonatomic, strong, readwrite) NSError *error; @property (nonatomic, strong, readwrite) NSData *body; @property (nonatomic, assign, readwrite) NSTimeInterval requestTime; @end @implementation SPTDataLoaderResponse #pragma mark Private + (instancetype)dataLoaderResponseWithRequest:(SPTDataLoaderRequest *)request response:(nullable NSURLResponse *)response { return [[self alloc] initWithRequest:request response:response]; } - (instancetype)initWithRequest:(SPTDataLoaderRequest *)request response:(nullable NSURLResponse *)response { self = [super init]; if (self) { _request = request; if ([response isKindOfClass:[NSHTTPURLResponse class]]) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; if (httpResponse.statusCode >= SPTDataLoaderResponseHTTPStatusCodeMovedMultipleChoices || httpResponse.statusCode <= SPTDataLoaderResponseHTTPStatusCodeSwitchProtocols) { _error = [NSError errorWithDomain:SPTDataLoaderResponseErrorDomain code:httpResponse.statusCode userInfo:nil]; } _resolvedURL = httpResponse.URL; _responseHeaders = httpResponse.allHeaderFields; _statusCode = httpResponse.statusCode; } _retryAfter = [self retryAfterForHeaders:_responseHeaders]; } return self; } - (BOOL)shouldRetry { if ([self.error.domain isEqualToString:SPTDataLoaderResponseErrorDomain]) { switch (self.error.code) { case SPTDataLoaderResponseHTTPStatusCodeInvalid: case SPTDataLoaderResponseHTTPStatusCodeContinue: case SPTDataLoaderResponseHTTPStatusCodeSwitchProtocols: case SPTDataLoaderResponseHTTPStatusCodeOK: case SPTDataLoaderResponseHTTPStatusCodeCreated: case SPTDataLoaderResponseHTTPStatusCodeAccepted: case SPTDataLoaderResponseHTTPStatusCodeNonAuthoritiveInformation: case SPTDataLoaderResponseHTTPStatusCodeNoContent: case SPTDataLoaderResponseHTTPStatusCodeResetContent: case SPTDataLoaderResponseHTTPStatusCodePartialContent: case SPTDataLoaderResponseHTTPStatusCodeMovedMultipleChoices: case SPTDataLoaderResponseHTTPStatusCodeMovedPermanently: case SPTDataLoaderResponseHTTPStatusCodeFound: case SPTDataLoaderResponseHTTPStatusCodeSeeOther: case SPTDataLoaderResponseHTTPStatusCodeNotModified: case SPTDataLoaderResponseHTTPStatusCodeUseProxy: case SPTDataLoaderResponseHTTPStatusCodeUnused: case SPTDataLoaderResponseHTTPStatusCodeTemporaryRedirect: case SPTDataLoaderResponseHTTPStatusCodeBadRequest: case SPTDataLoaderResponseHTTPStatusCodeUnauthorised: case SPTDataLoaderResponseHTTPStatusCodePaymentRequired: case SPTDataLoaderResponseHTTPStatusCodeForbidden: case SPTDataLoaderResponseHTTPStatusCodeMethodNotAllowed: case SPTDataLoaderResponseHTTPStatusCodeNotAcceptable: case SPTDataLoaderResponseHTTPStatusCodeProxyAuthenticationRequired: case SPTDataLoaderResponseHTTPStatusCodeConflict: case SPTDataLoaderResponseHTTPStatusCodeGone: case SPTDataLoaderResponseHTTPStatusCodeLengthRequired: // We always include the content-length header case SPTDataLoaderResponseHTTPStatusCodePreconditionFailed: case SPTDataLoaderResponseHTTPStatusCodeRequestEntityTooLarge: case SPTDataLoaderResponseHTTPStatusCodeRequestURITooLong: case SPTDataLoaderResponseHTTPStatusCodeRequestRangeUnsatisifiable: case SPTDataLoaderResponseHTTPStatusCodeExpectationFail: case SPTDataLoaderResponseHTTPStatusCodeHTTPVersionNotSupported: case SPTDataLoaderResponseHTTPStatusCodeNotImplemented: case SPTDataLoaderResponseHTTPStatusCodeNotFound: return NO; case SPTDataLoaderResponseHTTPStatusCodeRequestTimeout: case SPTDataLoaderResponseHTTPStatusCodeUnsupportedMediaTypes: case SPTDataLoaderResponseHTTPStatusCodeInternalServerError: case SPTDataLoaderResponseHTTPStatusCodeBadGateway: case SPTDataLoaderResponseHTTPStatusCodeServiceUnavailable: case SPTDataLoaderResponseHTTPStatusCodeGatewayTimeout: return YES; } } if ([self.error.domain isEqualToString:NSURLErrorDomain]) { switch (self.error.code) { case NSURLErrorCancelled: case NSURLErrorUnknown: case NSURLErrorBadURL: case NSURLErrorUnsupportedURL: case NSURLErrorZeroByteResource: case NSURLErrorCannotDecodeRawData: case NSURLErrorCannotDecodeContentData: case NSURLErrorCannotParseResponse: case NSURLErrorFileDoesNotExist: case NSURLErrorNoPermissionsToReadFile: case NSURLErrorDataLengthExceedsMaximum: case NSURLErrorRedirectToNonExistentLocation: case NSURLErrorBadServerResponse: case NSURLErrorUserCancelledAuthentication: case NSURLErrorUserAuthenticationRequired: case NSURLErrorServerCertificateHasBadDate: case NSURLErrorServerCertificateUntrusted: case NSURLErrorServerCertificateHasUnknownRoot: case NSURLErrorServerCertificateNotYetValid: case NSURLErrorClientCertificateRejected: case NSURLErrorClientCertificateRequired: return NO; case NSURLErrorTimedOut: case NSURLErrorCannotFindHost: case NSURLErrorCannotConnectToHost: case NSURLErrorNetworkConnectionLost: case NSURLErrorDNSLookupFailed: case NSURLErrorHTTPTooManyRedirects: case NSURLErrorResourceUnavailable: case NSURLErrorNotConnectedToInternet: case NSURLErrorSecureConnectionFailed: case NSURLErrorCannotLoadFromNetwork: return YES; } } return NO; } - (nullable NSDate *)retryAfterForHeaders:(NSDictionary<NSString *, NSString *> *)headers { static NSDateFormatter *httpDateFormatter; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ httpDateFormatter = [NSDateFormatter new]; [httpDateFormatter setDateFormat:@"EEE, dd MMM yyyy HH:mm:ss zzz"]; }); NSTimeInterval retryAfterSeconds = [headers[SPTDataLoaderResponseHeaderRetryAfter] doubleValue]; if (retryAfterSeconds != 0.0) { return [NSDate dateWithTimeIntervalSinceNow:retryAfterSeconds]; } NSString *retryAfterValue = headers[SPTDataLoaderResponseHeaderRetryAfter]; return [httpDateFormatter dateFromString:retryAfterValue]; } - (NSString *)description { return [NSString stringWithFormat:@"<%@: %p URL = \"%@\"; status-code = %ld; headers = %@>", self.class, (void *)self, self.resolvedURL, (long)self.statusCode, self.responseHeaders]; } @end NS_ASSUME_NONNULL_END