Source/TNLResponse.m (805 lines of code) (raw):

// // TNLResponse.m // TwitterNetworkLayer // // Created on 5/23/14. // Copyright © 2020 Twitter, Inc. All rights reserved. // #import <mach/mach_time.h> #import "NSCoder+TNLAdditions.h" #import "NSDictionary+TNLAdditions.h" #import "NSURLResponse+TNLAdditions.h" #import "NSURLSessionTaskMetrics+TNLAdditions.h" #import "TNL_Project.h" #import "TNLAttemptMetaData.h" #import "TNLAttemptMetrics_Project.h" #import "TNLHTTP.h" #import "TNLRequest.h" #import "TNLResponse_Project.h" #import "TNLTemporaryFile_Project.h" #import "TNLTiming.h" @implementation TNLResponse @synthesize operationError = _operationError; @synthesize originalRequest = _originalRequest; @synthesize info = _info; @synthesize metrics = _metrics; + (instancetype)responseWithResponse:(TNLResponse *)response { TNLResponse *newResponse = [[[self class] alloc] initInternalWithRequest:response.originalRequest operationError:response.operationError info:response.info metrics:response.metrics]; [newResponse prepare]; return newResponse; } + (instancetype)responseWithRequest:(id<TNLRequest>)originalRequest operationError:(NSError *)operationError info:(TNLResponseInfo *)info metrics:(TNLResponseMetrics *)metrics { TNLResponse *response = [[[self class] alloc] initInternalWithRequest:originalRequest operationError:operationError info:info metrics:metrics]; [response prepare]; [metrics finalizeMetrics]; return response; } - (instancetype)initInternalWithRequest:(id<TNLRequest>)originalRequest operationError:(NSError *)operationError info:(TNLResponseInfo *)info metrics:(TNLResponseMetrics *)metrics { if (self = [super init]) { _originalRequest = [originalRequest conformsToProtocol:@protocol(NSCopying)] ? [(NSObject *)originalRequest copy] : originalRequest; _operationError = operationError; _info = info; _metrics = metrics; } return self; } - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; abort(); return nil; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { NSError *operationError = [aDecoder decodeObjectOfClass:[NSError class] forKey:@"operationError"]; TNLResponseInfo *info = [aDecoder decodeObjectOfClass:[TNLResponseInfo class] forKey:@"info"]; TNLResponseMetrics *metrics = [aDecoder decodeObjectOfClass:[TNLResponseMetrics class] forKey:@"metrics"]; id<TNLRequest> request = nil; { NSString *originalRequestClassName = [aDecoder decodeObjectOfClass:[NSString class] forKey:@"originalRequestClass"]; Class originalRequestClass = Nil; if (originalRequestClassName != nil) { // we have an archived "request class name", try to convert into a concrete class to decode // does the decoder support "class name to class" mapping? if ([aDecoder respondsToSelector:@selector(classForClassName:)]) { // has mapping support, load via mapping originalRequestClass = [(id)aDecoder classForClassName:originalRequestClassName]; } // did we not load a mapping and have a class level mapping? if (Nil == originalRequestClass && [[aDecoder class] respondsToSelector:@selector(classForClassName:)]) { // has class level mapping support, load via mapping originalRequestClass = [[aDecoder class] classForClassName:originalRequestClassName]; } // did we not load via any mappings? if (Nil == originalRequestClass) { // load directly via NSClassFromString originalRequestClass = NSClassFromString(originalRequestClassName); } } // If we have a class... if (originalRequestClass != Nil) { // ... try to decode the request request = [aDecoder decodeObjectOfClass:originalRequestClass forKey:@"originalRequest"]; } } self = [self initInternalWithRequest:request operationError:operationError info:info metrics:metrics]; if (self) { [self prepare]; [metrics finalizeMetrics]; } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:TNLErrorToSecureCodingError(_operationError) forKey:@"operationError"]; [aCoder encodeObject:_info forKey:@"info"]; [aCoder encodeObject:_metrics forKey:@"metrics"]; id<TNLRequest, NSSecureCoding> originalRequest = nil; if ([_originalRequest conformsToProtocol:@protocol(NSSecureCoding)] && [[_originalRequest class] supportsSecureCoding]) { originalRequest = (id<TNLRequest, NSSecureCoding>)_originalRequest; } else { originalRequest = [[TNLResponseEncodedRequest alloc] initWithSourceRequest:_originalRequest]; } [aCoder encodeObject:NSStringFromClass([originalRequest class]) forKey:@"originalRequestClass"]; [aCoder encodeObject:originalRequest forKey:@"originalRequest"]; } - (void)prepare { } + (BOOL)supportsSecureCoding { return YES; } - (NSString *)description { NSMutableString *description = [NSMutableString stringWithFormat:@"<%@: %p {", NSStringFromClass([self class]), self]; NSString *requestDescription = nil; if (self.originalRequest) { if ([self.originalRequest respondsToSelector:@selector(URL)]) { requestDescription = [[self.originalRequest URL] description]; } else { requestDescription = NSStringFromClass([self.originalRequest class]); } } [description appendFormat:@" Request: %@", requestDescription]; [description appendFormat:@", HTTP: %ld", (long)self.info.statusCode]; if (self.operationError) { [description appendFormat:@", Operation-Error: %@", self.operationError]; } if (self.info.source == TNLResponseSourceLocalCache) { [description appendString:@", Cache-Hit: YES"]; } if (self.metrics) { [description appendFormat:@", Metrics: %@", self.metrics]; } [description appendString:@" }>"]; return description; } - (NSUInteger)hash { return (NSUInteger)((NSInteger)self.operationError.domain.hash + self.operationError.code) + self.info.hash + self.metrics.hash; // ignore the original request hash } - (BOOL)isEqual:(id)object { if ([super isEqual:object]) { return YES; } TNLResponse *other = object; if (![other isKindOfClass:[TNLResponse class]]) { return NO; } if (!TNLSecureCodingErrorsAreEqual(self.operationError, other.operationError)) { return NO; } if (self.originalRequest != nil) { if ([self.originalRequest respondsToSelector:@selector(isEqualToRequest:)]) { if (![self.originalRequest isEqualToRequest:other.originalRequest]) { return NO; } } else { if (!TNLRequestEqualToRequest(self.originalRequest, other.originalRequest, YES /*quickBodyCheck*/)) { return NO; } } } else if (other.originalRequest != nil) { return NO; } IS_EQUAL_OBJ_PROP_CHECK(self, other, info); IS_EQUAL_OBJ_PROP_CHECK(self, other, metrics); return YES; } @end @implementation TNLResponseInfo { @protected NSString *_rawRetryAfterValue; id _parsedRetryAfterValue; NSDate *_retryAfterDate; NSDictionary *_cachedLowercaseHeaderFields; } - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; abort(); } - (instancetype)initWithFinalURLRequest:(NSURLRequest *)finalURLRequest URLResponse:(NSHTTPURLResponse *)URLResponse source:(TNLResponseSource)source data:(NSData *)data temporarySavedFile:(id<TNLTemporaryFile>)temporarySavedFile { if (self = [super init]) { _URLResponse = URLResponse; _finalURLRequest = [finalURLRequest copy]; _data = data; _temporarySavedFile = temporarySavedFile; _source = source; _cachedLowercaseHeaderFields = [URLResponse.allHeaderFields tnl_copyWithLowercaseKeys]; { // We want to precache the retry after date on construction. // This is because the "Retry-After" header could provide a "delay from now" value (in seconds) // which we'll want to apply to the current time ([NSDate dateWithTimeIntervalSinceNow:retryAfterDelayInSecondsInteger]) // and not some future time. (void)self.retryAfterDate; } } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { NSHTTPURLResponse *URLResponse = [aDecoder decodeObjectOfClass:[NSURLResponse class] forKey:@"URLResponse"]; NSURLRequest *URLRequest = [aDecoder decodeObjectOfClass:[NSURLRequest class] forKey:@"finalURLRequest"]; TNLResponseSource source = [aDecoder decodeIntegerForKey:@"source"]; NSData *data = [aDecoder decodeObjectOfClass:[NSData class] forKey:@"data"]; id<TNLTemporaryFile> temporarySavedFile = nil; NSString *temporarySavedFilePath = [aDecoder decodeObjectOfClass:[NSString class] forKey:@"temporarySavedFilePath"]; if (temporarySavedFilePath) { temporarySavedFile = [[TNLExpiredTemporaryFile alloc] initWithFilePath:(temporarySavedFilePath.length > 0) ? temporarySavedFilePath : nil]; } return [self initWithFinalURLRequest:URLRequest URLResponse:URLResponse source:source data:data temporarySavedFile:temporarySavedFile]; } - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:_URLResponse forKey:@"URLResponse"]; [aCoder encodeObject:_finalURLRequest forKey:@"finalURLRequest"]; [aCoder encodeInteger:_source forKey:@"source"]; [aCoder encodeObject:_data forKey:@"data"]; NSString *temporarySavedFilePath = nil; if (_temporarySavedFile) { if ([(NSObject *)_temporarySavedFile respondsToSelector:@selector(path)]) { temporarySavedFilePath = [(id)_temporarySavedFile path]; } else { temporarySavedFilePath = @""; } } [aCoder encodeObject:temporarySavedFilePath forKey:@"temporarySavedFilePath"]; } + (BOOL)supportsSecureCoding { return YES; } - (NSUInteger)hash { return self.URLResponse.hash + self.finalURLRequest.hash + (NSUInteger)self.source + self.data.hash + ((self.temporarySavedFile == nil) ? 11 : 3); } - (BOOL)isEqual:(id)object { if ([super isEqual:object]) { return YES; } TNLResponseInfo *other = object; if (![other isKindOfClass:[TNLResponseInfo class]]) { return NO; } if (self.URLResponse) { if (![self.URLResponse tnl_isEqualToResponse:other.URLResponse]) { return NO; } } else if (other.URLResponse) { return NO; } IS_EQUAL_OBJ_PROP_CHECK(self, other, finalURLRequest); if (self.source != other.source) { return NO; } if ((self.temporarySavedFile != nil) || (other.temporarySavedFile != nil)) { // No 2 temporary saved files are "equal" so if either TNLResponseInfo has a temp file, // they are not equal return NO; } IS_EQUAL_OBJ_PROP_CHECK(self, other, data); return YES; } @end @implementation TNLResponseInfo (Convenience) - (TNLHTTPStatusCode)statusCode { return _URLResponse.statusCode; } - (NSURL *)finalURL { return _URLResponse.URL ?: _finalURLRequest.URL; } - (NSDictionary *)allHTTPHeaderFields { return _URLResponse.allHeaderFields; } - (NSString *)valueForResponseHeaderField:(NSString *)headerField { if (!headerField) { return nil; } return _cachedLowercaseHeaderFields[[headerField lowercaseString]]; } - (nullable NSDictionary<NSString *, NSString *> *)allHTTPHeaderFieldsWithLowerCaseKeys { return _cachedLowercaseHeaderFields; } @end @implementation TNLResponseEncodedRequest { NSURL *_URL; TNLHTTPMethod _HTTPMethodValue; NSDictionary<NSString *, NSString *> *_allHTTPHeaderFields; } + (BOOL)supportsSecureCoding { return YES; } - (instancetype)initWithSourceRequest:(id<TNLRequest>)request { if (self = [super init]) { _encodedSourceRequestHadBody = TNLRequestHasBody(request); _encodedSourceRequestClassName = [NSStringFromClass([request class]) copy]; _URL = request.URL; _HTTPMethodValue = TNLRequestGetHTTPMethodValue(request); _allHTTPHeaderFields = [request respondsToSelector:@selector(allHTTPHeaderFields)] ? [request.allHTTPHeaderFields copy] : nil; } return self; } - (instancetype)initWithCoder:(NSCoder *)coder { if (self = [super init]) { _encodedSourceRequestHadBody = [coder decodeBoolForKey:@"hasBody"]; _encodedSourceRequestClassName = [coder decodeObjectOfClass:[NSString class] forKey:@"sourceClass"]; _URL = [coder decodeObjectOfClass:[NSURL class] forKey:@"URL"]; _HTTPMethodValue = [coder decodeIntegerForKey:@"HTTPMethod"]; _allHTTPHeaderFields = [coder decodeObjectOfClass:[NSDictionary class] forKey:@"HTTPHeaders"]; } return self; } - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeBool:_encodedSourceRequestHadBody forKey:@"hasBody"]; [coder encodeObject:_encodedSourceRequestClassName forKey:@"sourceClass"]; [coder encodeObject:_URL forKey:@"URL"]; [coder encodeInteger:_HTTPMethodValue forKey:@"HTTPMethod"]; [coder encodeObject:_allHTTPHeaderFields forKey:@"HTTPHeaders"]; } - (nullable NSURL *)URL { return _URL; } - (TNLHTTPMethod)HTTPMethodValue { return _HTTPMethodValue; } - (nullable NSDictionary<NSString *, NSString *> *)allHTTPHeaderFields { return _allHTTPHeaderFields; } - (nullable NSData *)HTTPBody { if (_encodedSourceRequestHadBody) { /* In order to preserve "isEqual" comparison between the encoded request and the source request, we need to have a non-empty body. We're arbitrarily using a valid JSON payload as the body, but it could really be any payload. */ return [@"{\"body\":true}" dataUsingEncoding:NSUTF8StringEncoding]; } return nil; } @end @implementation TNLResponseMetrics { BOOL _final; NSArray<TNLAttemptMetrics *> *_attemptMetrics; } @synthesize attemptMetrics = _attemptMetrics; - (instancetype)init { return [self initWithEnqueueDate:[NSDate dateWithTimeIntervalSince1970:0] enqueueTime:0 completeDate:nil completeTime:0 attemptMetrics:nil]; } - (instancetype)initWithEnqueueDate:(NSDate *)enqueueDate enqueueTime:(uint64_t)enqueueTime completeDate:(nullable NSDate *)completeDate completeTime:(uint64_t)completeTime attemptMetrics:(nullable NSArray<TNLAttemptMetrics *> *)attemptMetrics { if (self = [super init]) { _final = NO; _enqueueDate = enqueueDate; _enqueueMachTime = enqueueTime; _completeDate = completeDate; _completeMachTime = completeTime; _attemptMetrics = [attemptMetrics mutableCopy] ?: [NSMutableArray array]; if (gTwitterNetworkLayerAssertEnabled) { if (_attemptMetrics.count > 0) { TNLAssert([_attemptMetrics[0] attemptType] == TNLAttemptTypeInitial); } } } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { NSArray<TNLAttemptMetrics *> *attemptMetrics = [aDecoder tnl_decodeArrayOfItemsOfClass:[TNLAttemptMetrics class] forKey:@"attemptMetrics"]; NSDate *enqueueDate = [aDecoder decodeObjectOfClass:[NSDate class] forKey:@"enqueueDate"] ?: [NSDate dateWithTimeIntervalSince1970:0]; const uint64_t enqueueTime = (uint64_t)[aDecoder decodeInt64ForKey:@"enqueueTime"]; const uint64_t completeTime = (uint64_t)[aDecoder decodeInt64ForKey:@"completeTime"]; NSDate *completeDate = [aDecoder decodeObjectOfClass:[NSDate class] forKey:@"completeDate"]; if (!completeDate && completeTime) { completeDate = [enqueueDate dateByAddingTimeInterval:TNLComputeDuration(enqueueTime, completeTime)]; } return [self initWithEnqueueDate:enqueueDate enqueueTime:enqueueTime completeDate:completeDate completeTime:completeTime attemptMetrics:attemptMetrics]; } - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:_attemptMetrics forKey:@"attemptMetrics"]; [aCoder encodeObject:_enqueueDate forKey:@"enqueueDate"]; [aCoder encodeInt64:(int64_t)_enqueueMachTime forKey:@"enqueueTime"]; [aCoder encodeObject:_completeDate forKey:@"completeDate"]; [aCoder encodeInt64:(int64_t)_completeMachTime forKey:@"completeTime"]; } - (void)finalizeMetrics { if (_final) { return; } _final = YES; _attemptMetrics = [_attemptMetrics copy]; [_attemptMetrics enumerateObjectsUsingBlock:^(TNLAttemptMetrics * _Nonnull metricObj, NSUInteger idx, BOOL * _Nonnull stop) { [metricObj finalizeMetrics]; // direct method, so we cannot use `makeObjectsPerformSelector:` }]; } + (BOOL)supportsSecureCoding { return YES; } - (NSUInteger)attemptCount { return _attemptMetrics.count; } - (NSUInteger)retryCount { NSUInteger count = 0; for (TNLAttemptMetrics *metrics in _attemptMetrics) { if (TNLAttemptTypeRetry == metrics.attemptType) { count++; } } return count; } - (NSUInteger)redirectCount { NSUInteger count = 0; for (TNLAttemptMetrics *metrics in _attemptMetrics) { if (TNLAttemptTypeRedirect == metrics.attemptType) { count++; } } return count; } - (void)didEnqueue { if (_final && _enqueueMachTime) { return; } _enqueueMachTime = mach_absolute_time(); _enqueueDate = [NSDate date]; } - (nullable NSDate *)firstAttemptStartDate { TNLAttemptMetrics *attemptMetrics = _attemptMetrics.firstObject; return attemptMetrics.startDate; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-implementations" - (uint64_t)firstAttemptStartMachTime #pragma clang diagnostic pop { TNLAttemptMetrics *attemptMetrics = _attemptMetrics.firstObject; return attemptMetrics.startMachTime; } - (nullable NSDate *)currentAttemptStartDate { TNLAttemptMetrics *attemptMetrics = _attemptMetrics.lastObject; return attemptMetrics.startDate; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-implementations" - (uint64_t)currentAttemptStartMachTime #pragma clang diagnostic pop { TNLAttemptMetrics *attemptMetrics = _attemptMetrics.lastObject; return attemptMetrics.startMachTime; } - (nullable NSDate *)currentAttemptEndDate { TNLAttemptMetrics *attemptMetrics = _attemptMetrics.lastObject; return attemptMetrics.endDate; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-implementations" - (uint64_t)currentAttemptEndMachTime #pragma clang diagnostic pop { TNLAttemptMetrics *attemptMetrics = _attemptMetrics.lastObject; return attemptMetrics.endMachTime; } - (void)setCompleteDate:(NSDate *)date machTime:(uint64_t)time { // support providing a complete time if not already set when already final if (_final && _completeMachTime) { return; } _completeMachTime = time; _completeDate = date; } - (void)addInitialStartWithDate:(NSDate *)date machTime:(uint64_t)machTime request:(NSURLRequest *)request { TNLAssert(_attemptMetrics.count == 0); [self _addAttemptStart:TNLAttemptTypeInitial date:date machTime:machTime request:request]; } - (void)addRetryStartWithDate:(NSDate *)date machTime:(uint64_t)machTime request:(NSURLRequest *)request { [self _addAttemptStart:TNLAttemptTypeRetry date:date machTime:machTime request:request]; } - (void)addRedirectStartWithDate:(NSDate *)date machTime:(uint64_t)machTime request:(NSURLRequest *)request { [self _addAttemptStart:TNLAttemptTypeRedirect date:date machTime:machTime request:request]; } - (void)_addAttemptStart:(TNLAttemptType)type date:(NSDate *)date machTime:(uint64_t)machTime request:(NSURLRequest *)request { if (_final) { return; } TNLAssert(request != nil); TNLAssert(_attemptMetrics != nil); TNLAssert(date != nil); TNLAttemptMetrics *lastMetrics = _attemptMetrics.lastObject; if (TNLAttemptTypeInitial == type) { TNLAssert(lastMetrics == nil); } else { TNLAssert(lastMetrics != nil); TNLAssert(lastMetrics.endMachTime != 0 && "addEnd:response:operationError: should have been called first!"); if (!lastMetrics.endMachTime) { [lastMetrics setEndDate:date machTime:machTime]; } } TNLAttemptMetrics *metrics = [[TNLAttemptMetrics alloc] initWithType:type startDate:date startMachTime:machTime endDate:nil endMachTime:0 metaData:nil URLRequest:request URLResponse:nil operationError:nil]; [(NSMutableArray *)_attemptMetrics addObject:metrics]; } - (void)updateCurrentRequest:(NSURLRequest *)request { TNLAttemptMetrics *metrics = _attemptMetrics.lastObject; [metrics updateRequest:request]; } - (void)addEndDate:(NSDate *)date machTime:(uint64_t)time response:(NSHTTPURLResponse *)response operationError:(NSError *)error { TNLAttemptMetrics *lastMetrics = _attemptMetrics.lastObject; if (lastMetrics && !lastMetrics.endMachTime) { [lastMetrics setEndDate:date machTime:time]; lastMetrics.URLResponse = response; lastMetrics.operationError = error; } } - (void)addMetaData:(TNLAttemptMetaData *)metaData taskMetrics:(NSURLSessionTaskMetrics *)taskMetrics { TNLAttemptMetrics *lastMetrics = _attemptMetrics.lastObject; if (lastMetrics && !lastMetrics.metaData) { lastMetrics.metaData = metaData; } if (taskMetrics) { const NSUInteger transactionMetricsCount = taskMetrics.transactionMetrics.count; const NSUInteger attemptMetricsCount = self.attemptMetrics.count; TNLAttemptType followingType = TNLAttemptTypeRedirect; NSUInteger taskMetricsIndex = 0; NSUInteger attemptMetricsIndex = 0; while (taskMetricsIndex < transactionMetricsCount && attemptMetricsIndex < attemptMetricsCount) { NSURLSessionTaskTransactionMetrics *transactionMetrics = taskMetrics.transactionMetrics[transactionMetricsCount - taskMetricsIndex - 1]; TNLAttemptMetrics *attemptMetrics = self.attemptMetrics[attemptMetricsCount - attemptMetricsIndex - 1]; const NSURLSessionTaskMetricsResourceFetchType expectedFetchType = attemptMetrics.metaData.localCacheHit ? NSURLSessionTaskMetricsResourceFetchTypeLocalCache : NSURLSessionTaskMetricsResourceFetchTypeNetworkLoad; if (expectedFetchType == transactionMetrics.resourceFetchType) { if (followingType != TNLAttemptTypeRedirect) { break; } [attemptMetrics setTaskTransactionMetrics:transactionMetrics]; followingType = attemptMetrics.attemptType; attemptMetricsIndex++; } taskMetricsIndex++; } } } - (NSDictionary *)dictionaryDescription:(BOOL)verbose { NSMutableDictionary *topDictionary = [NSMutableDictionary dictionary]; topDictionary[@"complete"] = (_completeMachTime != 0) ? @"true" : @"false"; topDictionary[@"duration"] = @(self.totalDuration); if (verbose) { topDictionary[@"attemptTime"] = @(self.currentAttemptDuration); topDictionary[@"queueTime"] = @(self.queuedDuration); topDictionary[@"allAttemptsTime"] = @(self.allAttemptsDuration); } NSMutableArray *attempts = [NSMutableArray arrayWithCapacity:self.attemptMetrics.count]; for (TNLAttemptMetrics *metrics in self.attemptMetrics) { NSDictionary *attemptDict = [metrics dictionaryDescription:verbose]; [attempts addObject:attemptDict]; } if (attempts.count > 0) { topDictionary[@"attempts"] = attempts; } return topDictionary; } - (NSString *)description { return [NSString stringWithFormat:@"<%@ %p: %@>", NSStringFromClass([self class]), self, [self dictionaryDescription:NO]]; } - (NSUInteger)hash { if (!_completeMachTime) { return NSUIntegerMax; } return (NSUInteger)(self.totalDuration * 1000ull); // the total duration is a good gauge for hashing } - (BOOL)isEqual:(id)object { if ([super isEqual:object]) { return YES; } TNLResponseMetrics *other = object; if (![other isKindOfClass:[TNLResponseMetrics class]]) { return NO; } if (self.attemptCount != other.attemptCount) { return NO; } TNLAssert(self.attemptCount == self.attemptMetrics.count); TNLAssert(other.attemptCount == other.attemptMetrics.count); NSDate *selfStartDate = self.firstAttemptStartDate; NSDate *otherStartDate = other.firstAttemptStartDate; for (NSUInteger i = 0; i < self.attemptCount; i++) { TNLAttemptMetrics *selfAttemptMetrics = self.attemptMetrics[i]; TNLAttemptMetrics *otherAttemptMetrics = other.attemptMetrics[i]; if (![selfAttemptMetrics isEqual:otherAttemptMetrics]) { return NO; } if (fabs([selfAttemptMetrics.startDate timeIntervalSinceDate:selfStartDate] - [otherAttemptMetrics.startDate timeIntervalSinceDate:otherStartDate]) > kTNLTimeEpsilon) { return NO; } } if (fabs(self.totalDuration - other.totalDuration) > kTNLTimeEpsilon) { return NO; } if (fabs(self.queuedDuration - other.queuedDuration) > kTNLTimeEpsilon) { return NO; } if (fabs(self.allAttemptsDuration - other.allAttemptsDuration) > kTNLTimeEpsilon) { return NO; } if (fabs(self.currentAttemptDuration - other.currentAttemptDuration) > kTNLTimeEpsilon) { return NO; } return YES; } - (NSTimeInterval)totalDuration { return [(_completeDate ?: [NSDate date]) timeIntervalSinceDate:_enqueueDate]; } - (NSTimeInterval)queuedDuration { return [(self.firstAttemptStartDate ?: [NSDate date]) timeIntervalSinceDate:_enqueueDate]; } - (NSTimeInterval)allAttemptsDuration { return [self.currentAttemptEndDate ?: [NSDate date] timeIntervalSinceDate:self.firstAttemptStartDate ?: [NSDate date]]; } - (NSTimeInterval)currentAttemptDuration { return [self.currentAttemptEndDate ?: [NSDate date] timeIntervalSinceDate:self.currentAttemptStartDate ?: [NSDate date]]; } - (TNLResponseMetrics *)deepCopyAndTrimIncompleteAttemptMetrics:(BOOL)trimIncompleteAttemptMetrics { NSMutableArray *dupeSubmetrics = [NSMutableArray arrayWithCapacity:_attemptMetrics.count]; for (TNLAttemptMetrics *submetric in _attemptMetrics) { if (trimIncompleteAttemptMetrics && !submetric.endMachTime) { break; } TNLAttemptMetrics *dupeSubmetric = [submetric copy]; [dupeSubmetrics addObject:dupeSubmetric]; } TNLResponseMetrics *metrics = [[TNLResponseMetrics alloc] initWithEnqueueDate:self.enqueueDate #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" enqueueTime:self.enqueueMachTime #pragma clang diagnostic pop completeDate:self.completeDate #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" completeTime:self.completeMachTime #pragma clang diagnostic pop attemptMetrics:dupeSubmetrics]; return metrics; } @end @implementation TNLResponseMetrics (UnitTesting) + (instancetype)fakeMetricsForDuration:(NSTimeInterval)duration URLRequest:(NSURLRequest *)request URLResponse:(nullable NSHTTPURLResponse *)URLResponse operationError:(nullable NSError *)error { uint64_t absoluteDiff = TNLAbsoluteFromTimeInterval(duration); NSDate *startDate = [NSDate dateWithTimeIntervalSince1970:0]; NSDate *endDate = [startDate dateByAddingTimeInterval:duration]; TNLResponseMetrics *metrics = [[TNLResponseMetrics alloc] initWithEnqueueDate:startDate enqueueTime:0 completeDate:endDate completeTime:absoluteDiff attemptMetrics:nil]; [metrics addInitialStartWithDate:startDate machTime:0 request:request]; [metrics addEndDate:endDate machTime:absoluteDiff response:URLResponse operationError:error]; return metrics; } @end @implementation TNLResponseInfo (RetryAfter) - (BOOL)hasRetryAfterHeader { return self.retryAfterRawValue != nil; } - (NSString *)retryAfterRawValue { if (!_rawRetryAfterValue) { _rawRetryAfterValue = [self valueForResponseHeaderField:@"Retry-After"] ?: (id)[NSNull null]; } return ([NSNull null] == (id)_rawRetryAfterValue) ? nil : _rawRetryAfterValue; } - (NSTimeInterval)retryAfterDelayFromNow { NSDate *retryAfterDate = self.retryAfterDate; if (!retryAfterDate) { return NSTimeIntervalSince1970; } return [retryAfterDate timeIntervalSinceDate:[NSDate date]]; } - (NSDate *)retryAfterDate { if (!_parsedRetryAfterValue) { NSString *retryAfterStringValue = self.retryAfterRawValue; _parsedRetryAfterValue = [NSHTTPURLResponse tnl_parseRetryAfterValueFromString:retryAfterStringValue]; if ([_parsedRetryAfterValue isKindOfClass:[NSNumber class]]) { _retryAfterDate = [NSDate dateWithTimeIntervalSinceNow:[(NSNumber *)_parsedRetryAfterValue doubleValue]]; } else if ([_parsedRetryAfterValue isKindOfClass:[NSDate class]]) { _retryAfterDate = _parsedRetryAfterValue; } else { _parsedRetryAfterValue = (id)[NSNull null]; if (retryAfterStringValue.length > 0) { TNLLogError(@"'Retry-After' header of response provided with invalid value: '%@'", retryAfterStringValue); } } } TNLAssert(_parsedRetryAfterValue != nil); return _retryAfterDate; } @end NSString *TNLAttemptTypeToString(TNLAttemptType type) { switch (type) { case TNLAttemptTypeInitial: return @"initial"; case TNLAttemptTypeRedirect: return @"redirect"; case TNLAttemptTypeRetry: return @"retry"; } return nil; }