Source/TNLRequestOperation.m (2,668 lines of code) (raw):

// // TNLRequestOperation.m // TwitterNetworkLayer // // Created on 5/23/14. // Copyright © 2020 Twitter, Inc. All rights reserved. // #include <mach/mach_time.h> #include <objc/message.h> #include <stdatomic.h> #import "NSCachedURLResponse+TNLAdditions.h" #import "NSDictionary+TNLAdditions.h" #import "NSURLRequest+TNLAdditions.h" #import "NSURLResponse+TNLAdditions.h" #import "NSURLSessionConfiguration+TNLAdditions.h" #import "NSURLSessionTaskMetrics+TNLAdditions.h" #import "TNL_Project.h" #import "TNLAttemptMetaData_Project.h" #import "TNLAttemptMetrics_Project.h" #import "TNLContentCoding.h" #import "TNLError.h" #import "TNLGlobalConfiguration_Project.h" #import "TNLHostSanitizer.h" #import "TNLPriority.h" #import "TNLRequest.h" #import "TNLRequestDelegate.h" #import "TNLRequestOperation_Project.h" #import "TNLRequestOperationCancelSource.h" #import "TNLRequestOperationQueue_Project.h" #import "TNLRequestRetryPolicyProvider.h" #import "TNLResponse_Project.h" #import "TNLSimpleRequestDelegate.h" #import "TNLTiming.h" #import "TNLURLSessionTaskOperation.h" NS_ASSUME_NONNULL_BEGIN #define TAG_FROM_METHOD(DELEGATE, PROTOCOL, SEL) [NSString stringWithFormat:@"%@<%@>->%@", NSStringFromClass([DELEGATE class]), NSStringFromProtocol(PROTOCOL), NSStringFromSelector(SEL)] static NSString * const kRedactedKeyValue = @"<redacted>"; static volatile atomic_uint_fast64_t sNextRetryId = ATOMIC_VAR_INIT(1); static dispatch_queue_t _RequestOperationDefaultCallbackQueue(void); static dispatch_queue_t _RequestOperationDefaultCallbackQueue() { static dispatch_queue_t sFallbackQueue; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sFallbackQueue = dispatch_queue_create("TNLRequestOperation.callback.queue", DISPATCH_QUEUE_SERIAL); }); return sFallbackQueue; } static dispatch_queue_t _RetryPolicyProviderQueue(id<TNLRequestRetryPolicyProvider> __nullable retryPolicyProvider); static dispatch_queue_t _RetryPolicyProviderQueue(id<TNLRequestRetryPolicyProvider> __nullable retryPolicyProvider) { dispatch_queue_t q = NULL; if ([retryPolicyProvider respondsToSelector:@selector(tnl_callbackQueue)]) { q = [retryPolicyProvider tnl_callbackQueue]; } if (!q) { q = _RequestOperationDefaultCallbackQueue(); } return q; } static dispatch_queue_t _URLSessionTaskOperationPropertyQueue(void); static dispatch_queue_t _URLSessionTaskOperationPropertyQueue() { static dispatch_queue_t sQueue; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sQueue = dispatch_queue_create("TNLRequestOperation.URLSessionTaskOperation.atomic.queue", DISPATCH_QUEUE_CONCURRENT); }); return sQueue; } TNL_OBJC_FINAL TNL_OBJC_DIRECT_MEMBERS @interface TNLTimerOperation : TNLSafeOperation - (instancetype)initWithDelay:(NSTimeInterval)delay; @end @interface TNLRequestOperation () // Private Properties #pragma twitter startignorestylecheck @property (tnl_nonatomic_direct, readonly, nullable) id<TNLRequestDelegate> internalDelegate; @property (tnl_atomic_direct, copy, nullable) NSString *cachedDelegateClassName; // annoyingly the Twitter style checker considers this a delegate, so we'll wrap it in the ignorestylecheck #pragma twitter endignorestylecheck @property (tnl_atomic_direct, nullable) NSError *terminalErrorOverride; @property (tnl_atomic_direct, readonly) TNLResponseSource responseSource; @property (tnl_nonatomic_direct, readonly) TNLRequestExecutionMode executionMode; @property (tnl_atomic_direct) TNLPriority internalPriority; @property (tnl_atomic_direct, nullable) TNLResponse *internalFinalResponse; // Private Writability @property (nonatomic, nullable) TNLRequestOperationQueue *requestOperationQueue; @property (nonatomic, nullable) id<TNLRequest> hydratedRequest; @property (nonatomic) float downloadProgress; @property (nonatomic) float uploadProgress; @property (atomic, nullable) TNLURLSessionTaskOperation *URLSessionTaskOperation; @property (atomic, copy, nullable) NSDictionary<NSString *, id<TNLContentDecoder>> *additionalDecoders; @property (atomic, copy, nullable) NSURLRequest *hydratedURLRequest; @end TNL_OBJC_DIRECT_MEMBERS @interface TNLRequestOperation (Network) // Methods that can only be called from the tnl_network_queue() #pragma mark NSOperation helpers - (void)_network_prepareToConnectThenConnect:(BOOL)isRetry; - (void)_network_connect:(BOOL)isRetry; - (void)_network_startURLSessionTaskOperation:(TNLURLSessionTaskOperation *)taskOp isRetry:(BOOL)isRetry; - (void)_network_fail:(NSError *)error; #pragma mark NSOperation - (void)_network_retryWithOldResponse:(TNLResponse *)oldResponse retryPolicyProvider:(nullable id<TNLRequestRetryPolicyProvider>)retryPolicyProvider; - (void)_network_prepareToStart; - (void)_network_start:(BOOL)isRetry; - (void)_network_cleanupAfterComplete; #pragma mark Private Methods - (void)_network_transitionToState:(TNLRequestOperationState)state withAttemptResponse:(nullable TNLResponse *)attemptResponse; - (void)_network_completeTransitionFromState:(TNLRequestOperationState)oldState toState:(TNLRequestOperationState)state withAttemptResponse:(nullable TNLResponse *)attemptResponse; - (TNLResponse *)_network_finalizeResponseWithInfo:(TNLResponseInfo *)responseInfo responseError:(nullable NSError *)responseError metadata:(nullable TNLAttemptMetaData *)metadata taskMetrics:(nullable NSURLSessionTaskMetrics *)taskMetrics; - (void)_network_applyEncodingMetricsToInfo:(TNLResponseInfo *)responseInfo withMetaData:(nullable TNLAttemptMetaData *)metadata; - (void)_network_updateMetricsFromState:(TNLRequestOperationState)oldState toState:(TNLRequestOperationState)newState withAttemptResponse:(nullable TNLResponse *)attemptResponse; - (void)_network_didCompleteAttemptWithResponse:(TNLResponse *)response disposition:(TNLAttemptCompleteDisposition)disposition; - (void)_network_completeWithResponse:(TNLResponse *)response; #pragma mark Attempt Retry // Primary "attempt retry" method - (void)_network_attemptRetryDuringTransitionFromState:(TNLRequestOperationState)oldState toState:(TNLRequestOperationState)state withAttemptResponse:(nullable TNLResponse *)attemptResponse; // Internal methods called by primary "attempt retry" method - (BOOL)_network_shouldAttemptRetryDuringTransitionFromState:(TNLRequestOperationState)oldState toState:(TNLRequestOperationState)state withAttemptResponse:(nullable TNLResponse *)attemptResponse; - (BOOL)_network_shouldForciblyRetryInvalidatedURLSessionRequestWithAttemptResponse:(TNLResponse *)attemptResponse; - (void)_network_forciblyRetryInvalidatedURLSessionRequestWithAttemptResponse:(TNLResponse *)attemptResponse; - (void)_network_retryDuringTransitionFromState:(TNLRequestOperationState)oldState toState:(TNLRequestOperationState)state withAttemptResponse:(TNLResponse *)attemptResponse retryPolicyProvider:(id<TNLRequestRetryPolicyProvider>)retryPolicyProvider; #pragma mark Retry - (void)_network_startRetryWithDelay:(NSTimeInterval)retryDelay oldResponse:(TNLResponse *)oldResponse retryPolicyProvider:(nullable id<TNLRequestRetryPolicyProvider>)retryPolicyProvider; - (void)_network_invalidateRetry; - (void)_network_tryRetryWithId:(uint64_t)retryId oldResponse:(TNLResponse *)oldResponse retryPolicyProvider:(nullable id<TNLRequestRetryPolicyProvider>)retryPolicyProvider; #pragma mark Operation Timeout Timer - (void)_network_startOperationTimeoutTimer:(NSTimeInterval)timeInterval; - (void)_network_invalidateOperationTimeoutTimer; - (void)_network_operationTimeoutTimerDidFire; #pragma mark Callback Timeout Timer - (void)_network_startCallbackTimerWithAlreadyElapsedDuration:(NSTimeInterval)alreadyElapsedTime; - (void)_network_startCallbackTimerIfNecessary; - (void)_network_stopCallbackTimer; - (void)_network_callbackTimerFired; #if TARGET_OS_IOS || TARGET_OS_TV - (void)_network_pauseCallbackTimer; - (void)_network_unpauseCallbackTimer; #endif #pragma mark Attempt Timeout Timer - (void)_network_startAttemptTimeoutTimer:(NSTimeInterval)timeInterval; - (void)_network_invalidateAttemptTimeoutTimer; - (void)_network_attemptTimeoutTimerDidFire; #pragma mark Application States (iOS only) #if TARGET_OS_IOS || TARGET_OS_TV - (void)_network_startObservingApplicationStates; - (void)_dealloc_stopObservingApplicationStatesIfNecessary; #endif - (void)_network_willResignActive; - (void)_network_didBecomeActive; #pragma mark Background (iOS only) - (void)_network_startBackgroundTask; - (void)_network_endBackgroundTask; #pragma mark State - (BOOL)_network_isStateActive; - (BOOL)_network_isStateFinished; - (BOOL)_network_isStateCancelled; - (BOOL)_network_hasFailed; - (BOOL)_network_hasFailedOrFinished; - (BOOL)_network_isPreparing; #pragma mark Preparation Methods /* Use static C functions instead of ObjC methods for simpler iteration while avoiding __TEXT binary overhead */ typedef void (^tnl_request_preparation_block_t)(void); static void _network_prepStep_validateOriginalRequest(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock); static void _network_prepStep_hydrateRequest(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock); static void _network_prepStep_validateHydratedRequest(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock); static void _network_prepStep_convertHydratedRequestToScratchURLRequest(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock); static void _network_prepStep_validateConfiguration(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock); static void _network_prepStep_applyGlobalHeadersToScratchURLRequest(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock); static void _network_prepStep_applyAcceptEncodingsToScratchURLRequest(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock); static void _network_prepStep_applyContentEncodingToScratchURLRequest(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock); static void _network_prepStep_sanitizeHostForScratchURLRequest(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock); static void _network_prepStep_authorizeScratchURLRequest(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock); static void _network_prepStep_cementScratchURLRequest(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock); - (void)_network_prepareRequestStep:(size_t)preparationStepIndex isRetry:(BOOL)isRetry; @end typedef void (*tnl_request_preparation_function_ptr)(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t block); static const tnl_request_preparation_function_ptr _Nonnull sPreparationFunctions[] = { _network_prepStep_validateOriginalRequest, _network_prepStep_hydrateRequest, _network_prepStep_validateHydratedRequest, _network_prepStep_convertHydratedRequestToScratchURLRequest, _network_prepStep_validateConfiguration, _network_prepStep_applyGlobalHeadersToScratchURLRequest, _network_prepStep_applyAcceptEncodingsToScratchURLRequest, _network_prepStep_applyContentEncodingToScratchURLRequest, _network_prepStep_sanitizeHostForScratchURLRequest, _network_prepStep_authorizeScratchURLRequest, _network_prepStep_cementScratchURLRequest, }; static const size_t kPreparationFunctionsCount = (sizeof(sPreparationFunctions) / sizeof(sPreparationFunctions[0])); TNL_OBJC_DIRECT_MEMBERS @interface TNLRequestOperation (Tagging) - (void)_updateTag:(NSString *)tag; - (void)_clearTag:(NSString *)tag; @end #pragma mark - TNLRequestOperation TNLStaticAssert(sizeof(TNLRequestOperationState_Unaligned_AtomicT) == sizeof(TNLRequestOperationState), enum_size_missmatch); @implementation TNLRequestOperation { dispatch_queue_t _callbackQueue; // could be concurrent, call with barrier dispatch_queue_t _completionQueue; // could be concurrent, call with barrier TNLPriority _enqueuedPriority; NSMutableArray *_callbackTagStack; uint64_t _mach_callbackTagTime; NSError *_cachedCancelError; id<TNLRequestDelegate> _strongDelegate; TNLBackgroundTaskIdentifier _backgroundTaskIdentifier; NSTimeInterval _cloggedCallbackTimeout; TNLRequestOperationState_AtomicT _state; NSMutableURLRequest *_scratchURLRequest; NSTimeInterval _scratchURLRequestEncodeLatency; SInt64 _scratchURLRequestOriginalBodyLength; SInt64 _scratchURLRequestEncodedBodyLength; id<TNLHostSanitizer> _hostSanitizer; TNLResponseMetrics *_metrics; // Timers dispatch_source_t _operationTimeoutTimerSource; dispatch_source_t _attemptTimeoutTimerSource; dispatch_source_t _callbackTimeoutTimerSource; uint64_t _callbackTimeoutTimerStartMachTime; uint64_t _callbackTimeoutTimerPausedMachTime; // Retry uint64_t _activeRetryId; // Flags that can only be written on the background queue struct { BOOL didEnqueue:1; BOOL didStart:1; BOOL didPrep:1; BOOL inRetryCheck:1; BOOL silentStart:1; BOOL isCallbackClogDetectionEnabled:1; BOOL isObservingApplicationStates:1; BOOL applicationIsInBackground:1; unsigned int invalidSessionRetryCount:4; } _backgroundFlags; // atomic properties support TNLURLSessionTaskOperation *_URLSessionTaskOperation; volatile atomic_bool _didCompleteFinishedCallback; } #pragma mark overrides with no behavior change - (void)addDependency:(NSOperation *)op { [super addDependency:op]; } - (void)waitUntilFinished { [super waitUntilFinished]; } #pragma mark init/dealloc #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-implementations" - (instancetype)init #pragma clang diagnostic pop { [self doesNotRecognizeSelector:_cmd]; abort(); } - (instancetype)initWithRequest:(nullable id<TNLRequest>)request responseClass:(nullable Class)responseClass configuration:(nullable TNLRequestConfiguration *)config delegate:(nullable id<TNLRequestDelegate>)delegate { if (self = [super init]) { TNLIncrementObjectCount([self class]); arc4random_buf(&_operationId, sizeof(int64_t)); _backgroundTaskIdentifier = TNLBackgroundTaskInvalid; atomic_init(&_state, TNLRequestOperationStateIdle); atomic_init(&_didCompleteFinishedCallback, false); _originalRequest = [request conformsToProtocol:@protocol(NSCopying)] ? [(NSObject *)request copy] : request; if (!config) { config = [TNLRequestConfiguration defaultConfiguration]; } _requestConfiguration = [config copy]; _requestDelegate = delegate; if ([delegate isKindOfClass:[TNLSimpleRequestDelegate class]]) { _strongDelegate = delegate; } else if (delegate) { _cachedDelegateClassName = NSStringFromClass([delegate class]); } _metrics = [[TNLResponseMetrics alloc] init]; _callbackTagStack = [[NSMutableArray alloc] init]; _cloggedCallbackTimeout = [TNLGlobalConfiguration sharedInstance].requestOperationCallbackTimeout; _backgroundFlags.isCallbackClogDetectionEnabled = _cloggedCallbackTimeout > 0.0 && _requestConfiguration.executionMode != TNLRequestExecutionModeBackground; _responseClass = [TNLResponse class]; if (responseClass) { if ([responseClass isSubclassOfClass:_responseClass]) { _responseClass = responseClass; } else { TNLLogError(@"%1$@ is not a subclass of %2$@! Using %2$@ instead", NSStringFromClass(responseClass), NSStringFromClass(_responseClass)); TNLAssert([responseClass isSubclassOfClass:_responseClass]); } } } return self; } - (TNLBackgroundTaskIdentifier)dealloc_backgroundTaskIdentifier TNL_THREAD_SANITIZER_DISABLED { return _backgroundTaskIdentifier; } - (BOOL)dealloc_isObservingApplicationStates TNL_THREAD_SANITIZER_DISABLED { return _backgroundFlags.isObservingApplicationStates; } - (void)dealloc { tnl_dispatch_timer_invalidate(_operationTimeoutTimerSource); tnl_dispatch_timer_invalidate(_attemptTimeoutTimerSource); tnl_dispatch_timer_invalidate(_callbackTimeoutTimerSource); _activeRetryId = 0; // invalidate any pending retry TNLBackgroundTaskIdentifier backgroundTaskIdentifier = self.dealloc_backgroundTaskIdentifier; if (TNLBackgroundTaskInvalid != backgroundTaskIdentifier) { [[TNLGlobalConfiguration sharedInstance] endBackgroundTaskWithIdentifier:backgroundTaskIdentifier]; } #if TARGET_OS_IOS || TARGET_OS_TV [self _dealloc_stopObservingApplicationStatesIfNecessary]; #endif TNLDecrementObjectCount([self class]); } #pragma mark Constructors + (instancetype)operationWithRequest:(nullable id<TNLRequest>)request responseClass:(nullable Class)responseClass configuration:(nullable TNLRequestConfiguration *)config delegate:(nullable id<TNLRequestDelegate>)delegate { return [[self alloc] initWithRequest:request responseClass:responseClass configuration:config delegate:delegate]; } #pragma mark Prep Methods - (void)enqueueToOperationQueue:(TNLRequestOperationQueue *)operationQueue { TNLAssert(!_requestOperationQueue); self.requestOperationQueue = operationQueue; tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ TNLAssert(!self->_backgroundFlags.didEnqueue); if (!self->_backgroundFlags.didEnqueue && atomic_load(&self->_state) == TNLRequestOperationStateIdle) { self->_enqueuedPriority = self.internalPriority; [self->_metrics didEnqueue]; self->_backgroundFlags.didEnqueue = YES; [self->_requestOperationQueue syncAddRequestOperation:self]; } }); } #pragma mark Properties - (nullable TNLURLSessionTaskOperation *)URLSessionTaskOperation { __block TNLURLSessionTaskOperation *op; dispatch_sync(_URLSessionTaskOperationPropertyQueue(), ^{ op = self->_URLSessionTaskOperation; }); return op; } - (void)setURLSessionTaskOperation:(nullable TNLURLSessionTaskOperation *)URLSessionTaskOperation { __block TNLURLSessionTaskOperation *oldOp = nil; dispatch_barrier_sync(_URLSessionTaskOperationPropertyQueue(), ^{ oldOp = self->_URLSessionTaskOperation; self->_URLSessionTaskOperation = URLSessionTaskOperation; }); if (oldOp) { [oldOp dissassociateRequestOperation:self]; } } #pragma mark Private Properties - (nullable id<TNLRequestDelegate>)internalDelegate { id<TNLRequestDelegate> delegate = _strongDelegate ?: _requestDelegate; NSString *delegateClassName = self.cachedDelegateClassName; if (!delegate && delegateClassName) { TNLLogWarning(@"The TNLRequestDelegate (%@) of this TNLRequestOperation (%p) is nil. It is possible the delegate was only held weakly and was deallocated unexpectedly. Either cancel the TNLRequestOperation from the delegate's dealloc or maintain a strong reference to the delegate when used with a TNLRequestOperation (like setting the delegate as the operation's context property).", delegateClassName, self); } return delegate; } - (TNLRequestExecutionMode)executionMode { return _requestConfiguration.executionMode; } - (TNLRequestOperationState)state { return atomic_load(&_state); } - (void)setState:(TNLRequestOperationState)state async:(BOOL)async { if (async) { tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ [self _tnl_setState:state]; }); } else { [self _tnl_setState:state]; } } - (void)_tnl_setState:(TNLRequestOperationState)state { [self willChangeValueForKey:@"state"]; atomic_store(&_state, state); if (TNLRequestOperationStateStarting == state && _backgroundFlags.silentStart) { // clear the silent start flag _backgroundFlags.silentStart = 0; } [self didChangeValueForKey:@"state"]; } - (void)setHydratedRequest:(nullable id<TNLRequest>)hydratedRequest { if (_hydratedRequest != hydratedRequest) { _hydratedRequest = [hydratedRequest conformsToProtocol:@protocol(NSCopying)] ? [(NSObject *)hydratedRequest copy] : hydratedRequest; } } #pragma mark Hybrid override and redirect properties - (nullable NSError *)error { return self.terminalErrorOverride ?: self.response.operationError ?: self.URLSessionTaskOperation.error; } #pragma mark Redirected Properties - (nullable TNLResponse *)response { return self.internalFinalResponse; } - (NSUInteger)attemptCount { return _metrics.attemptCount; } - (NSUInteger)retryCount { return _metrics.retryCount; } - (NSUInteger)redirectCount { return _metrics.redirectCount; } - (nullable NSURLRequest *)currentURLRequest { return self.URLSessionTaskOperation.currentURLRequest ?: self.URLSessionTaskOperation.originalURLRequest; } - (nullable NSHTTPURLResponse *)currentURLResponse { return self.URLSessionTaskOperation.URLResponse; } - (TNLResponseSource)responseSource { return self.URLSessionTaskOperation.responseSource; } #pragma mark NSOperation Overrides - (NSOperationQueuePriority)queuePriority { return TNLConvertTNLPriorityToQueuePriority(_backgroundFlags.didEnqueue ? _enqueuedPriority : self.internalPriority); } - (NSQualityOfService)qualityOfService { return TNLConvertTNLPriorityToQualityOfService(_backgroundFlags.didEnqueue ? _enqueuedPriority : self.internalPriority); } #pragma mark Priority - (void)setPriority:(TNLPriority)priority { tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ if (self.internalPriority != priority) { BOOL didEnqueue = self->_backgroundFlags.didEnqueue; // cannot modify other NSOperation priorities if we've already been enqueued BOOL qos = NO; if (!didEnqueue) { qos = [self respondsToSelector:@selector(setQualityOfService:)]; [self willChangeValueForKey:@"queuePriority"]; if (qos) { [self willChangeValueForKey:@"qualityOfService"]; } } self.internalPriority = priority; if (!didEnqueue) { if (qos) { [self didChangeValueForKey:@"qualityOfService"]; } [self didChangeValueForKey:@"queuePriority"]; } [self.URLSessionTaskOperation network_priorityDidChangeForRequestOperation:self]; } }); } - (TNLPriority)priority { return self.internalPriority; } #pragma mark Project Methods - (void)network_URLSessionTaskOperationIsWaitingForConnectivity:(TNLURLSessionTaskOperation *)taskOp { TNLAssertIsNetworkQueue(); if (![self _network_hasFailedOrFinished] && self.URLSessionTaskOperation == taskOp) { // Invalidate timeout timer if configured to do so if (TNL_BITMASK_INTERSECTS_FLAGS(_requestConfiguration.connectivityOptions, TNLRequestConnectivityOptionInvalidateAttemptTimeoutWhenWaitForConnectivityTriggered)) { [self _network_invalidateAttemptTimeoutTimer]; } // Send event id<TNLRequestEventHandler> eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperationIsWaitingForConnectivity:); if ([eventHandler respondsToSelector:callback]) { tnl_dispatch_barrier_async_autoreleasing(_callbackQueue, ^{ NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); [self _updateTag:tag]; [eventHandler tnl_requestOperationIsWaitingForConnectivity:self]; [self _clearTag:tag]; }); } } } - (void)network_URLSessionTaskOperation:(TNLURLSessionTaskOperation *)taskOp didReceiveURLResponse:(NSURLResponse *)URLResponse { TNLAssertIsNetworkQueue(); if (![self _network_hasFailedOrFinished] && self.URLSessionTaskOperation == taskOp) { id<TNLRequestEventHandler> eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:didReceiveURLResponse:); if ([eventHandler respondsToSelector:callback]) { tnl_dispatch_barrier_async_autoreleasing(_callbackQueue, ^{ NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); [self _updateTag:tag]; [eventHandler tnl_requestOperation:self didReceiveURLResponse:URLResponse]; [self _clearTag:tag]; }); } } } - (void)network_URLSessionTaskOperation:(TNLURLSessionTaskOperation *)taskOp willPerformRedirectFromRequest:(NSURLRequest *)fromRequest withHTTPResponse:(NSHTTPURLResponse *)response toRequest:(NSURLRequest *)toRequest completion:(TNLRequestRedirectCompletionBlock)completion { TNLAssertIsNetworkQueue(); // provide the redirect policy [self _network_willPerformRedirectFromRequest:fromRequest withHTTPResponse:response toRequest:toRequest forTaskOperation:taskOp redirectPolicy:_requestConfiguration.redirectPolicy completion:completion]; } - (void)_network_willPerformRedirectFromRequest:(NSURLRequest *)fromRequest withHTTPResponse:(NSHTTPURLResponse *)response toRequest:(NSURLRequest *)providedToRequest forTaskOperation:(TNLURLSessionTaskOperation *)taskOp redirectPolicy:(TNLRequestRedirectPolicy)redirectPolicy completion:(TNLRequestRedirectCompletionBlock)completion TNL_OBJC_DIRECT { TNLAssertIsNetworkQueue(); if (![self _network_hasFailedOrFinished] && self.URLSessionTaskOperation == taskOp) { NSURLRequest *toRequest = providedToRequest; switch (redirectPolicy) { case TNLRequestRedirectPolicyDontRedirect: toRequest = nil; break; case TNLRequestRedirectPolicyDoRedirect: // permit redirect break; case TNLRequestRedirectPolicyRedirectToSameHost: if (![fromRequest.URL.host.lowercaseString isEqualToString:toRequest.URL.host.lowercaseString]) { toRequest = nil; } break; case TNLRequestRedirectPolicyUseCallback: { id<TNLRequestRedirecter> redirecter = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:willRedirectFromRequest:withResponse:toRequest:completion:); if ([redirecter respondsToSelector:callback]) { tnl_dispatch_barrier_async_autoreleasing(_callbackQueue, ^{ NSString *tag = TAG_FROM_METHOD(redirecter, @protocol(TNLRequestRedirecter), callback); [self _updateTag:tag]; [redirecter tnl_requestOperation:self willRedirectFromRequest:fromRequest withResponse:response toRequest:toRequest completion:^(id<TNLRequest> finalToRequest) { [self _clearTag:tag]; // all `TNLURLSessionTaskOperationDelegate` completion blocks must be called from tnl_network_queue tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ completion(finalToRequest); }); }]; }); } else { // No callback to call, revert to Default behavior TNLLogWarning(@"Use callback specified in redirect policy but %@ not implemented in delegate (%@)", NSStringFromProtocol(@protocol(TNLRequestRedirecter)), redirecter); [self _network_willPerformRedirectFromRequest:fromRequest withHTTPResponse:response toRequest:toRequest forTaskOperation:taskOp redirectPolicy:TNLRequestRedirectPolicyDefault completion:completion]; } return; } } completion(toRequest); } else { completion(nil); } } - (void)network_URLSessionTaskOperation:(TNLURLSessionTaskOperation *)taskOp redirectedFrom:(NSURLRequest *)fromRequest withHTTPResponse:(NSHTTPURLResponse *)response to:(NSURLRequest *)toRequest metaData:(TNLAttemptMetaData *)metaData { TNLAssertIsNetworkQueue(); if (![self _network_hasFailedOrFinished] && self.URLSessionTaskOperation == taskOp) { // Capture info from attempt NSDate *dateNow = [NSDate date]; const uint64_t machTime = mach_absolute_time(); [_metrics addMetaData:metaData taskMetrics:nil]; [_metrics addEndDate:dateNow machTime:machTime response:response operationError:nil]; [_metrics addRedirectStartWithDate:dateNow machTime:machTime request:toRequest]; TNLResponseMetrics *metrics = [_metrics deepCopyAndTrimIncompleteAttemptMetrics:YES]; TNLResponseInfo *info = [[TNLResponseInfo alloc] initWithFinalURLRequest:fromRequest URLResponse:response source:((response.tnl_wasCachedResponse) ? TNLResponseSourceLocalCache : TNLResponseSourceNetworkRequest) data:nil temporarySavedFile:nil]; TNLResponse *placeholderResponse = [self.responseClass responseWithRequest:self.originalRequest operationError:nil info:info metrics:metrics]; // Complete attempt [self _network_didCompleteAttemptWithResponse:placeholderResponse disposition:TNLAttemptCompleteDispositionRedirecting]; [self.requestOperationQueue operation:self didStartAttemptWithMetrics:_metrics.attemptMetrics.lastObject]; // Event the redirect id<TNLRequestEventHandler> eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:didRedirectFromURLRequest:toURLRequest:); if ([eventHandler respondsToSelector:callback]) { tnl_dispatch_barrier_async_autoreleasing(_callbackQueue, ^{ NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); [self _updateTag:tag]; [eventHandler tnl_requestOperation:self didRedirectFromURLRequest:fromRequest toURLRequest:toRequest]; [self _clearTag:tag]; }); } } } - (void)_network_notifySanitizedHost:(NSString *)oldHost toHost:(NSString *)newHost TNL_OBJC_DIRECT { TNLAssertIsNetworkQueue(); id<TNLRequestEventHandler> eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:didSanitizeFromHost:toHost:); if ([eventHandler respondsToSelector:callback]) { tnl_dispatch_barrier_async_autoreleasing(_callbackQueue, ^{ NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); [self _updateTag:tag]; [eventHandler tnl_requestOperation:self didSanitizeFromHost:oldHost toHost:newHost]; [self _clearTag:tag]; }); } } - (void)network_URLSessionTaskOperation:(TNLURLSessionTaskOperation *)taskOp redirectFromRequest:(NSURLRequest *)fromRequest withHTTPResponse:(NSHTTPURLResponse *)response to:(NSURLRequest *)toRequest completionHandler:(void (^)(NSURLRequest * __nullable, NSError * __nullable))completionHandler { TNLAssertIsNetworkQueue(); if (_hostSanitizer) { NSString *host = toRequest.URL.host; [_hostSanitizer tnl_host:host wasEncounteredForURLRequest:toRequest asRedirect:YES completion:^(TNLHostSanitizerBehavior behavior, NSString *newHost) { tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ TNLAssert([host isEqualToString:toRequest.URL.host]); NSError *error = nil; NSMutableURLRequest *mRequest = [toRequest mutableCopy]; const TNLHostReplacementResult hostReplacementResult = [mRequest tnl_replaceURLHost:newHost behavior:behavior error:&error]; if (TNLHostReplacementResultSuccess == hostReplacementResult) { [self _network_notifySanitizedHost:host toHost:newHost]; } else { mRequest = nil; } if (error) { [self _network_fail:error]; } completionHandler(mRequest ?: toRequest, error); }); }]; } else { completionHandler(toRequest, nil); } } - (void)network_URLSessionTaskOperation:(TNLURLSessionTaskOperation *)taskOp didUpdateUploadProgress:(float)progress { TNLAssertIsNetworkQueue(); // Progress can exceed 1.0, cap it if (progress > 1.0f) { progress = 1.0f; } if (self.URLSessionTaskOperation != taskOp || _uploadProgress == progress) { return; } self.uploadProgress = progress; if (![self _network_hasFailedOrFinished]) { id<TNLRequestEventHandler> eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:didUpdateUploadProgress:); if ([eventHandler respondsToSelector:callback]) { tnl_dispatch_barrier_async_autoreleasing(_callbackQueue, ^{ NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); [self _updateTag:tag]; [eventHandler tnl_requestOperation:self didUpdateUploadProgress:progress]; [self _clearTag:tag]; }); } } } - (void)network_URLSessionTaskOperation:(TNLURLSessionTaskOperation *)taskOp didUpdateDownloadProgress:(float)progress { TNLAssertIsNetworkQueue(); // Progress can exceed 1.0, cap it if (progress > 1.0f) { progress = 1.0f; } if (self.URLSessionTaskOperation != taskOp || self->_downloadProgress == progress) { return; } self.downloadProgress = progress; if (![self _network_hasFailedOrFinished]) { id<TNLRequestEventHandler> eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:didUpdateDownloadProgress:); if ([eventHandler respondsToSelector:callback]) { tnl_dispatch_barrier_async_autoreleasing(_callbackQueue, ^{ NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); [self _updateTag:tag]; [eventHandler tnl_requestOperation:self didUpdateDownloadProgress:progress]; [self _clearTag:tag]; }); } } } - (void)network_URLSessionTaskOperation:(TNLURLSessionTaskOperation *)taskOp appendReceivedData:(NSData *)data { TNLAssertIsNetworkQueue(); if (![self _network_hasFailedOrFinished] && self.URLSessionTaskOperation == taskOp) { switch (_requestConfiguration.responseDataConsumptionMode) { case TNLResponseDataConsumptionModeChunkToDelegateCallback: { id<TNLRequestEventHandler> eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:didReceiveData:); if ([eventHandler respondsToSelector:callback]) { tnl_dispatch_barrier_async_autoreleasing(self->_callbackQueue, ^{ NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); [self _updateTag:tag]; [eventHandler tnl_requestOperation:self didReceiveData:data]; [self _clearTag:tag]; }); } break; } case TNLResponseDataConsumptionModeNone: case TNLResponseDataConsumptionModeStoreInMemory: case TNLResponseDataConsumptionModeSaveToDisk: TNLAssertNever(); break; } } } - (void)network_URLSessionTaskOperation:(TNLURLSessionTaskOperation *)taskOp didStartTaskWithTaskIdentifier:(NSUInteger)taskId configIdentifier:(nullable NSString *)configIdentifier sharedContainerIdentifier:(nullable NSString *)sharedContainerIdentifier isBackgroundRequest:(BOOL)isBackgroundRequest { TNLAssertIsNetworkQueue(); if (![self _network_hasFailedOrFinished] && self.URLSessionTaskOperation == taskOp) { TNLAssert((self.executionMode == TNLRequestExecutionModeBackground) == isBackgroundRequest); id<TNLRequestEventHandler> eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:didStartRequestWithURLSessionTaskIdentifier:URLSessionConfigurationIdentifier:URLSessionSharedContainerIdentifier:isBackgroundRequest:); if ([eventHandler respondsToSelector:callback]) { tnl_dispatch_barrier_async_autoreleasing(_callbackQueue, ^{ NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); [self _updateTag:tag]; [eventHandler tnl_requestOperation:self didStartRequestWithURLSessionTaskIdentifier:taskId URLSessionConfigurationIdentifier:configIdentifier URLSessionSharedContainerIdentifier:sharedContainerIdentifier isBackgroundRequest:isBackgroundRequest]; [self _clearTag:tag]; }); } } } - (void)network_URLSessionTaskOperation:(TNLURLSessionTaskOperation *)taskOp finalizeWithResponseInfo:(TNLResponseInfo *)responseInfo responseError:(nullable NSError *)responseError metaData:(TNLAttemptMetaData *)metadata taskMetrics:(nullable NSURLSessionTaskMetrics *)taskMetrics completion:(TNLRequestMakeFinalResponseCompletionBlock)completion { TNLAssertIsNetworkQueue(); if (self.URLSessionTaskOperation != taskOp || [self _network_hasFailedOrFinished]) { completion(nil); return; } TNLResponse *response = [self _network_finalizeResponseWithInfo:responseInfo responseError:responseError metadata:metadata taskMetrics:taskMetrics]; completion(response); } - (void)network_URLSessionTaskOperation:(TNLURLSessionTaskOperation *)taskOp didTransitionToState:(TNLRequestOperationState)state withResponse:(nullable TNLResponse *)response { TNLAssertIsNetworkQueue(); TNLAssert(state != TNLRequestOperationStateIdle); if (self.URLSessionTaskOperation != taskOp || [self _network_hasFailedOrFinished]) { return; } [self _network_transitionToState:state withAttemptResponse:response]; } - (void)network_URLSessionTaskOperation:(TNLURLSessionTaskOperation *)taskOp didStartSessionTaskWithRequest:(NSURLRequest *)request { TNLAssertIsNetworkQueue(); if (self.URLSessionTaskOperation != taskOp || [self _network_hasFailedOrFinished]) { return; } TNLRequestOperationState state = atomic_load(&_state); if (TNLRequestOperationStateStarting == state) { [_metrics updateCurrentRequest:request]; } } #pragma mark Wait - (void)waitUntilFinishedWithoutBlockingRunLoop { // Default implementation is to block the thread until the execution completes. // This can deadlock if the caller is not careful and the completion queue or callback queue // are the same thread that waitUntilFinished are called from. // In this method, we'll pump the run loop until we're finished as a way to provide an alternative. NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; if (!runLoop) { return [self waitUntilFinished]; } while (!self.isFinished) { [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.3]]; } } #pragma mark Cancel - (void)cancel { [self cancelWithSource:[[TNLOperationCancelMethodCancelSource alloc] init] underlyingError:nil]; } - (void)cancelWithSource:(id<TNLRequestOperationCancelSource>)source { [self cancelWithSource:source underlyingError:nil]; } - (void)cancelWithSource:(id<TNLRequestOperationCancelSource>)source underlyingError:(nullable NSError *)optionalUnderlyingError { NSParameterAssert(source != nil); // A cancel can easily be followed by a weak delegate being deallocated. // Clear our cached delegate class name so that we don't warn about the delegate being nil. self.cachedDelegateClassName = nil; tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ if (self->_cachedCancelError || [self _network_hasFailedOrFinished]) { return; } NSError *error = TNLErrorFromCancelSource(source, optionalUnderlyingError); self->_cachedCancelError = error; [self _network_fail:error]; }); } #pragma mark NSOperation - (BOOL)isConcurrent { return YES; } - (BOOL)isAsynchronous { return YES; } - (BOOL)isFinished { return [self _network_isStateFinished] && atomic_load(&_didCompleteFinishedCallback); } - (BOOL)isCancelled { return [self _network_isStateCancelled]; } - (BOOL)isExecuting { if ([self _network_isStateActive]) { return YES; } if (TNLRequestOperationStateIsFinal(atomic_load(&_state))) { return !atomic_load(&_didCompleteFinishedCallback); } return NO; } - (void)start { tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ TNLAssert(!self->_backgroundFlags.didStart); TNLAssert(self->_backgroundFlags.didEnqueue); TNLAssert(self->_requestOperationQueue); TNLAssert(self->_requestConfiguration); if ([self _network_hasFailedOrFinished]) { // might have been pre-emptively cancelled or failed return; } [self _network_prepareToStart]; self->_backgroundFlags.didStart = YES; [self->_requestOperationQueue operationDidStart:self]; [self _network_startOperationTimeoutTimer:self->_requestConfiguration.operationTimeout]; TNLAssert(self->_metrics.attemptCount == 0); if (self->_cachedCancelError) { [self _network_fail:self->_cachedCancelError]; } else { [self _network_start:NO /*isRetry*/]; } }); } @end #pragma mark - TNLRequestOperation (Network) @implementation TNLRequestOperation (Network) #pragma mark Operation State Accessors - (BOOL)_network_isStateFinished { return TNLRequestOperationStateIsFinal(atomic_load(&_state)); } - (BOOL)_network_isStateCancelled { return TNLRequestOperationStateCancelled == atomic_load(&_state); } - (BOOL)_network_isStateActive { return TNLRequestOperationStateIsActive(atomic_load(&_state)); } - (BOOL)_network_hasFailed { return self.terminalErrorOverride != nil; } - (BOOL)_network_hasFailedOrFinished { return [self _network_hasFailed] || [self _network_isStateFinished]; } - (BOOL)_network_isPreparing { return self.state == TNLRequestOperationStatePreparingRequest && ![self _network_hasFailedOrFinished]; } #pragma mark Preparation Methods - (void)_network_prepareRequestStep:(size_t)preparationStepIndex isRetry:(BOOL)isRetry { if (![self _network_isPreparing]) { return; } if (preparationStepIndex >= kPreparationFunctionsCount) { [self _network_connect:isRetry]; return; } tnl_request_preparation_function_ptr prepareStep = sPreparationFunctions[preparationStepIndex]; prepareStep(self, ^{ [self _network_prepareRequestStep:preparationStepIndex+1 isRetry:isRetry]; }); } static void _network_prepStep_validateOriginalRequest(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock) { if (!self) { return; } TNLAssert(nextBlock != nil); TNLAssert([self _network_isPreparing]); id<TNLRequest> originalRequest = self.originalRequest; NSError *error = nil; if (!originalRequest) { error = TNLErrorCreateWithCode(TNLErrorCodeRequestOperationRequestNotProvided); } if (error) { [self _network_fail:error]; } else { nextBlock(); } } static void _network_prepStep_hydrateRequest(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock) { if (!self) { return; } TNLAssert(nextBlock != nil); TNLAssert([self _network_isPreparing]); id<TNLRequestHydrater> hydrater = self.internalDelegate; id<TNLRequest> originalRequest = self.originalRequest; SEL callback = @selector(tnl_requestOperation:hydrateRequest:completion:); tnl_dispatch_barrier_async_autoreleasing(self->_callbackQueue, ^{ if ([hydrater respondsToSelector:callback]) { NSString *tag = TAG_FROM_METHOD(hydrater, @protocol(TNLRequestHydrater), callback); [self _updateTag:tag]; [hydrater tnl_requestOperation:self hydrateRequest:originalRequest completion:^(id<TNLRequest> hydratedRequest, NSError *error) { [self _clearTag:tag]; tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ if (![self _network_isPreparing]) { return; } if (error) { [self _network_fail:TNLErrorCreateWithCodeAndUnderlyingError(TNLErrorCodeRequestOperationFailedToHydrateRequest, error)]; } else { self.hydratedRequest = hydratedRequest ?: originalRequest; nextBlock(); } }); }]; } else { tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ self.hydratedRequest = originalRequest; nextBlock(); }); } }); } static void _network_prepStep_validateHydratedRequest(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock) { if (!self) { return; } TNLAssert(nextBlock != nil); TNLAssert([self _network_isPreparing]); id<TNLRequest> hydratedRequest = self.hydratedRequest; NSError *underlyingError; // Validate the request itself const BOOL isValid = TNLRequestValidate(hydratedRequest, self->_requestConfiguration, &underlyingError); if (!isValid) { [self _network_fail:TNLErrorCreateWithCodeAndUnderlyingError(TNLErrorCodeRequestOperationInvalidHydratedRequest, underlyingError)]; return; } nextBlock(); } static void _network_prepStep_convertHydratedRequestToScratchURLRequest(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock) { if (!self) { return; } TNLAssert(nextBlock != nil); TNLAssert([self _network_isPreparing]); NSError *error = nil; id<TNLRequest> request = self.hydratedRequest; NSMutableURLRequest *mURLRequest = TNLRequestToNSMutableURLRequest(request, self.requestConfiguration, &error); if (!mURLRequest) { [self _network_fail:error]; return; } self->_scratchURLRequest = mURLRequest; self->_scratchURLRequestEncodeLatency = 0; self->_scratchURLRequestOriginalBodyLength = 0; self->_scratchURLRequestEncodedBodyLength = 0; nextBlock(); } static void _network_prepStep_validateConfiguration(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock) { if (!self) { return; } TNLAssert(nextBlock != nil); TNLAssert([self _network_isPreparing]); TNLRequestConfiguration *config = self->_requestConfiguration; const BOOL hasAttemptTimeout = config.attemptTimeout >= MIN_TIMER_INTERVAL; const BOOL hasIdleTimeout = config.idleTimeout >= MIN_TIMER_INTERVAL; const BOOL hasOperationTimeout = config.operationTimeout >= MIN_TIMER_INTERVAL; if (hasAttemptTimeout && hasIdleTimeout && (config.attemptTimeout - config.idleTimeout < -0.05)) { TNLLogWarning(@"Attempt Timeout (%.2f) should not be shorter than the Idle Timeout (%.2f)!", config.attemptTimeout, config.idleTimeout); } if (hasOperationTimeout && hasAttemptTimeout && (config.operationTimeout - config.attemptTimeout < -0.05)) { TNLLogWarning(@"Operation Timeout (%.2f) should not be shorter than the Attempt Timeout (%.2f)!", config.operationTimeout, config.attemptTimeout); } if (config.executionMode == TNLRequestExecutionModeBackground) { if (config.redirectPolicy != TNLRequestRedirectPolicyDoRedirect) { NSString *message = @"The operation will execute in the background and follow all redirects however the operation's configuration specified a policy that differs. Operation will continue by ignoring the policy and following redirects."; TNLLogWarning(@"%@", message); TNLAssertMessage(config.redirectPolicy == TNLRequestRedirectPolicyDoRedirect, @"%@", message); } } nextBlock(); } static void _network_prepStep_applyGlobalHeadersToScratchURLRequest(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock) { if (!self) { return; } TNLAssert(nextBlock != nil); TNLAssert([self _network_isPreparing]); NSArray<id<TNLHTTPHeaderProvider>> *headerProviders = [TNLRequestOperationQueue allGlobalHeaderProviders]; if (headerProviders.count > 0) { // Since HTTP headers are case-insensitive, // we want to use the setValue:forHTTPHeaderField: // for every single header to use the built in // case-insensitive behavior built into NSURLRequest // Pull out the dictionaries NSDictionary<NSString *, NSString *> *existingHeaders = self->_scratchURLRequest.allHTTPHeaderFields; NSMutableDictionary<NSString *, NSString *> *defaultHeaders = [[NSMutableDictionary alloc] init]; NSMutableDictionary<NSString *, NSString *> *overrideHeaders = [[NSMutableDictionary alloc] init]; NSURLRequest *immutableScratchRequest = [self->_scratchURLRequest copy]; for (id<TNLHTTPHeaderProvider> headerProvider in headerProviders) { NSDictionary<NSString *, NSString *> *tmpDict; if ([headerProvider respondsToSelector:@selector(tnl_allDefaultHTTPHeaderFieldsForRequest:URLRequest:)]) { tmpDict = [headerProvider tnl_allDefaultHTTPHeaderFieldsForRequest:self->_originalRequest URLRequest:immutableScratchRequest]; [defaultHeaders addEntriesFromDictionary:tmpDict]; } if ([headerProvider respondsToSelector:@selector(tnl_allOverrideHTTPHeaderFieldsForRequest:URLRequest:)]) { tmpDict = [headerProvider tnl_allOverrideHTTPHeaderFieldsForRequest:self->_originalRequest URLRequest:immutableScratchRequest]; [overrideHeaders addEntriesFromDictionary:tmpDict]; } } // Clear the headers on the request to start self->_scratchURLRequest.allHTTPHeaderFields = nil; // 1) default headers [defaultHeaders enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull obj, BOOL * _Nonnull stop) { [self->_scratchURLRequest setValue:obj forHTTPHeaderField:key]; }]; // 2) specified headers [existingHeaders enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull obj, BOOL * _Nonnull stop) { [self->_scratchURLRequest setValue:obj forHTTPHeaderField:key]; }]; // 3) override headers [overrideHeaders enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull obj, BOOL * _Nonnull stop) { [self->_scratchURLRequest setValue:obj forHTTPHeaderField:key]; }]; } nextBlock(); } static void _network_prepStep_applyAcceptEncodingsToScratchURLRequest(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock) { if (!self) { return; } TNLAssert(nextBlock != nil); TNLAssert([self _network_isPreparing]); tnl_defer(nextBlock); // Do we do decoding? switch (self->_requestConfiguration.responseDataConsumptionMode) { case TNLResponseDataConsumptionModeStoreInMemory: case TNLResponseDataConsumptionModeChunkToDelegateCallback: break; default: // won't decode return; } switch (self->_requestConfiguration.executionMode) { case TNLRequestExecutionModeInApp: case TNLRequestExecutionModeInAppBackgroundTask: break; default: // won't decode return; } // Store the decoders that we'll use NSArray<id<TNLContentDecoder>> *additionalDecoders = self->_requestConfiguration.additionalContentDecoders; BOOL didSetAdditionalDecoders = NO; NSMutableSet<NSString *> *decoderTypes = [NSMutableSet setWithCapacity:additionalDecoders.count + 3]; if ([NSURLSessionConfiguration tnl_URLSessionSupportsDecodingBrotliContentEncoding]) { [decoderTypes addObject:@"br"]; // supported by default on recent OSes } [decoderTypes addObject:@"gzip"]; // supported by default [decoderTypes addObject:@"deflate"]; // supported by default if (additionalDecoders.count > 0) { NSMutableDictionary<NSString *, id<TNLContentDecoder>> *decoders = [[NSMutableDictionary alloc] initWithCapacity:additionalDecoders.count]; for (id<TNLContentDecoder> decoder in additionalDecoders) { NSString *decoderType = [[decoder tnl_contentEncodingType] lowercaseString]; if (![decoderTypes containsObject:decoderType]) { [decoderTypes addObject:decoderType]; decoders[decoderType] = decoder; } } if (decoders.count > 0) { self.additionalDecoders = [decoders copy]; didSetAdditionalDecoders = YES; } else { self.additionalDecoders = nil; } } else { self.additionalDecoders = nil; } NSString *HTTPHeaderDecoderTypesString = [[self->_scratchURLRequest valueForHTTPHeaderField:@"Accept-Encoding"] lowercaseString]; if (!HTTPHeaderDecoderTypesString) { // No Accept-Encoding set if (didSetAdditionalDecoders) { // Set the Accept-Encoding to our supported decoders NSArray<NSString *> *sortedDecoderTypes = [decoderTypes.allObjects sortedArrayUsingSelector:@selector(compare:)]; HTTPHeaderDecoderTypesString = [sortedDecoderTypes componentsJoinedByString:@", "]; [self->_scratchURLRequest setValue:HTTPHeaderDecoderTypesString forHTTPHeaderField:@"Accept-Encoding"]; } } if (gTwitterNetworkLayerAssertEnabled && didSetAdditionalDecoders) { // A custom set of Accept-Encodings were provided... let's validate (but not fail) NSMutableSet<NSString *> *HTTPHeaderDecoderTypesSet = nil; NSArray<NSString *> *HTTPHeaderDecoderTypes = [HTTPHeaderDecoderTypesString componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@", "]]; if (HTTPHeaderDecoderTypes.count) { HTTPHeaderDecoderTypesSet = [NSMutableSet setWithCapacity:(HTTPHeaderDecoderTypes.count / 2) + 1]; for (NSString *decoderType in HTTPHeaderDecoderTypes) { NSArray<NSString *> *decoderTypeComponents = [decoderType componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"; "]]; NSString *decoderTypeTrue = decoderTypeComponents.firstObject; if (decoderTypeTrue.length) { if ([decoderType isEqualToString:@"*"]) { // ballsy! will take anything in response TNLLogWarning(@"%@ has `Accept-Encoding: %@` - this can be overly accepting!", self->_originalRequest, HTTPHeaderDecoderTypesString); return; } [HTTPHeaderDecoderTypesSet addObject:decoderTypeTrue]; } } } NSMutableSet<NSString *> *diff = [HTTPHeaderDecoderTypesSet mutableCopy]; [diff minusSet:decoderTypes]; if (diff.count > 0) { NSArray<NSString *> *sortedDecoderTypes = [decoderTypes.allObjects sortedArrayUsingSelector:@selector(compare:)]; NSString *decoderTypesString = [sortedDecoderTypes componentsJoinedByString:@", "]; TNLLogWarning(@"%@ has `Accept-Encoding: %@` - but only has specified decoders for `%@`", self->_originalRequest, HTTPHeaderDecoderTypesString, decoderTypesString); } } } static void _network_prepStep_applyContentEncodingToScratchURLRequest(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock) { if (!self) { return; } TNLAssert(nextBlock != nil); TNLAssert([self _network_isPreparing]); // Body to encode? NSData *body = self->_scratchURLRequest.HTTPBody; if (!body.length) { nextBlock(); return; } // Encoder to encode with? id<TNLContentEncoder> encoder = self->_requestConfiguration.contentEncoder; if (!encoder) { nextBlock(); return; } // If there's a preset "Content-Encoding", can we handle it? NSString *encoderType = [[encoder tnl_contentEncodingType] lowercaseString]; NSString *HTTPHeaderEncoderType = [[self->_scratchURLRequest valueForHTTPHeaderField:@"Content-Encoding"] lowercaseString]; if (HTTPHeaderEncoderType && ![HTTPHeaderEncoderType isEqualToString:encoderType]) { [self _network_fail:TNLErrorCreateWithCode(TNLErrorCodeRequestOperationRequestContentEncodingTypeMissMatch)]; return; } // Jump to coding queue tnl_dispatch_async_autoreleasing(tnl_coding_queue(), ^{ // Do encoding const uint64_t startMachTime = mach_absolute_time(); NSError *encoderError; NSData *encodedData = [encoder tnl_encodeHTTPBody:body error:&encoderError]; const NSTimeInterval encodeLatency = TNLComputeDuration(startMachTime, mach_absolute_time()); const BOOL skipEncoding = (encoderError.code == TNLContentEncodingErrorCodeSkipEncoding) && [encoderError.domain isEqualToString:TNLContentEncodingErrorDomain]; // Back to network queue tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ // Error? if (!encodedData && !skipEncoding) { [self _network_fail:TNLErrorCreateWithCodeAndUnderlyingError(TNLErrorCodeRequestOperationRequestContentEncodingFailed, encoderError)]; return; } // Success! if (skipEncoding) { self->_scratchURLRequest.HTTPBody = body; [self->_scratchURLRequest setValue:nil forHTTPHeaderField:@"Content-Encoding"]; } else { const NSUInteger originalLength = body.length; const NSUInteger encodedLength = encodedData.length; self->_scratchURLRequest.HTTPBody = encodedData; [self->_scratchURLRequest setValue:encoderType forHTTPHeaderField:@"Content-Encoding"]; self->_scratchURLRequestEncodeLatency = encodeLatency; self->_scratchURLRequestOriginalBodyLength = (SInt64)originalLength; self->_scratchURLRequestEncodedBodyLength = (SInt64)encodedLength; #if DEBUG const double ratio = (encodedLength) ? (double)originalLength / (double)encodedLength : 0; TNLLogDebug(@"%@ compression ratio: %f", self->_scratchURLRequest.URL, ratio); #endif } nextBlock(); }); }); } static void _network_prepStep_sanitizeHostForScratchURLRequest(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock) { if (!self) { return; } TNLAssert(nextBlock != nil); TNLAssert([self _network_isPreparing]); NSString *host = self->_scratchURLRequest.URL.host; self->_hostSanitizer = (self->_requestConfiguration.skipHostSanitization) ? nil : [TNLGlobalConfiguration sharedInstance].hostSanitizer; if (self->_hostSanitizer) { [self->_hostSanitizer tnl_host:host wasEncounteredForURLRequest:[self->_scratchURLRequest copy] asRedirect:NO completion:^(TNLHostSanitizerBehavior behavior, NSString *newHost) { tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ TNLAssert([host isEqualToString:self->_scratchURLRequest.URL.host]); NSError *error = nil; const TNLHostReplacementResult hostReplacementResult = [self->_scratchURLRequest tnl_replaceURLHost:newHost behavior:behavior error:&error]; if (TNLHostReplacementResultSuccess == hostReplacementResult) { [self _network_notifySanitizedHost:host toHost:newHost]; } if (error) { [self _network_fail:error]; } else { nextBlock(); } }); }]; } else { nextBlock(); } } static void _network_prepStep_authorizeScratchURLRequest(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock) { if (!self) { return; } TNLAssert(nextBlock != nil); TNLAssert([self _network_isPreparing]); id<TNLRequestAuthorizer> authorizer = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:authorizeURLRequest:completion:); if (!authorizer || ![authorizer respondsToSelector:callback]) { nextBlock(); return; } tnl_dispatch_barrier_async_autoreleasing(self->_callbackQueue, ^{ NSString *tag = TAG_FROM_METHOD(authorizer, @protocol(TNLRequestAuthorizer), callback); [self _updateTag:tag]; [authorizer tnl_requestOperation:self authorizeURLRequest:[self->_scratchURLRequest copy] completion:^(NSString *authHeader, NSError *error) { [self _clearTag:tag]; tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ if (![self _network_isPreparing]) { return; } if (error) { [self _network_fail:TNLErrorCreateWithCodeAndUnderlyingError(TNLErrorCodeRequestOperationFailedToAuthorizeRequest, error)]; return; } if (authHeader) { [self->_scratchURLRequest setValue:(authHeader.length > 0) ? authHeader : nil forHTTPHeaderField:@"Authorization"]; } nextBlock(); }); }]; }); } static void _network_prepStep_cementScratchURLRequest(TNLRequestOperation * __nullable const self, tnl_request_preparation_block_t nextBlock) { if (!self) { return; } TNLAssert(nextBlock != nil); TNLAssert([self _network_isPreparing]); self.hydratedURLRequest = self->_scratchURLRequest; self->_scratchURLRequest = nil; nextBlock(); } #pragma mark NSOperation helpers - (void)_network_prepareToConnectThenConnect:(BOOL)isRetry { [self _network_prepareRequestStep:0 isRetry:isRetry]; } - (void)_network_connect:(BOOL)isRetry { TNLAssert([self _network_isPreparing]); TNLAssertMessage(self.URLSessionTaskOperation == nil, @"Already have a TNLURLSessionTaskOperation? state = %@", TNLRequestOperationStateToString(self.state)); // Do not update the `.state` here. // The `.URLSessionTaskOperation` will update to `TNLRequestOperationStateStarting` once it starts // (which may be delayed by 503 backoffs). [self.requestOperationQueue findURLSessionTaskOperationForRequestOperation:self complete:^(TNLURLSessionTaskOperation *taskOp) { tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ [self _network_startURLSessionTaskOperation:taskOp isRetry:isRetry]; }); }]; } - (void)_network_startURLSessionTaskOperation:(TNLURLSessionTaskOperation *)taskOp isRetry:(BOOL)isRetry { if ([self _network_hasFailedOrFinished]) { return; } self.URLSessionTaskOperation = taskOp; id<TNLRequestEventHandler> eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:readyToEnqueueUnderlyingNetworkingOperation:enqueueBlock:); if (![eventHandler respondsToSelector:callback]) { [taskOp enqueueToOperationQueueIfNeeded:self.requestOperationQueue]; return; } tnl_dispatch_barrier_async_autoreleasing(_callbackQueue, ^{ NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); [self _updateTag:tag]; [eventHandler tnl_requestOperation:self readyToEnqueueUnderlyingNetworkingOperation:isRetry enqueueBlock:^(NSArray<NSOperation *> *dependencies) { [self _clearTag:tag]; // add dependencies synchronously with callback if (dependencies) { TNLLogDebug(@"Added dependencies to %@: %@", taskOp, dependencies); for (NSOperation *op in dependencies) { [taskOp addDependency:op]; } } // dispatch to network queue to start the op tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ [taskOp enqueueToOperationQueueIfNeeded:self.requestOperationQueue]; }); }]; }); } - (void)_network_fail:(NSError *)error { [self _network_prepareToStart]; // in case we fail before we start if ([self _network_hasFailed]) { return; } if (!_backgroundFlags.didStart) { return; } if ([self _network_isStateFinished]) { // something else failed first, abort this subsequent attempt to fail return; } TNLAssert(error != nil); BOOL isCancel = NO; BOOL isTerminal = NO; if ([error.domain isEqualToString:TNLErrorDomain]) { isTerminal = TNLErrorCodeIsTerminal(error.code); isCancel = (error.code == TNLErrorCodeRequestOperationCancelled); } if (isTerminal) { self.terminalErrorOverride = error; } TNLResponseInfo *info = [[TNLResponseInfo alloc] initWithFinalURLRequest:self.currentURLRequest URLResponse:self.currentURLResponse source:self.responseSource data:nil temporarySavedFile:nil]; TNLAttemptMetaData *metadata; NSURLSessionTaskMetrics *taskMetrics; TNLURLSessionTaskOperation *URLSessionTaskOperation = self.URLSessionTaskOperation; if (URLSessionTaskOperation) { metadata = [URLSessionTaskOperation network_metaDataWithLowerCaseHeaderFields:info.allHTTPHeaderFieldsWithLowerCaseKeys]; taskMetrics = [URLSessionTaskOperation network_taskMetrics]; } else { metadata = (TNLAttemptMetaData * __nonnull)nil; taskMetrics = nil; } TNLResponse *response = [self _network_finalizeResponseWithInfo:info responseError:error metadata:metadata taskMetrics:taskMetrics]; if ([self _network_isStateFinished]) { TNLAssertNever(); return; } [self _network_transitionToState:(isCancel) ? TNLRequestOperationStateCancelled : TNLRequestOperationStateFailed withAttemptResponse:response]; // discard the task operation at the end so all internal states can be updated first before disassociating with the associated task operation self.URLSessionTaskOperation = nil; } #pragma mark NSOperation - (void)_network_retryWithOldResponse:(TNLResponse *)oldResponse retryPolicyProvider:(nullable id<TNLRequestRetryPolicyProvider>)retryPolicyProvider { if ([self _network_hasFailedOrFinished]) { return; } TNLAssertMessage(TNLRequestOperationStateWaitingToRetry == atomic_load(&_state), @"Actual state is %@", TNLRequestOperationStateToString(atomic_load(&_state))); self.downloadProgress = 0.0; self.uploadProgress = 0.0; [self _network_start:YES /*isRetry*/]; if (!_backgroundFlags.silentStart) { SEL callback = @selector(tnl_requestOperation:didStartRetryFromResponse:); if ([retryPolicyProvider respondsToSelector:callback]) { tnl_dispatch_barrier_async_autoreleasing(_RetryPolicyProviderQueue(retryPolicyProvider), ^{ NSString *tag = TAG_FROM_METHOD(retryPolicyProvider, @protocol(TNLRequestRetryPolicyProvider), callback); [self _updateTag:tag]; [retryPolicyProvider tnl_requestOperation:self didStartRetryFromResponse:oldResponse]; [self _clearTag:tag]; }); } id<TNLRequestEventHandler> eventHandler = self.internalDelegate; callback = @selector(tnl_requestOperation:didStartRetryFromResponse:policyProvider:); if ([eventHandler respondsToSelector:callback]) { tnl_dispatch_barrier_async_autoreleasing(_callbackQueue, ^{ NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); [self _updateTag:tag]; [eventHandler tnl_requestOperation:self didStartRetryFromResponse:oldResponse policyProvider:retryPolicyProvider]; [self _clearTag:tag]; }); } } } - (void)_network_prepareToStart { if (!_backgroundFlags.didStart) { if (!_backgroundFlags.didPrep) { id<TNLRequestDelegate> delegate = self.internalDelegate; // Get the callback queue _callbackQueue = [delegate respondsToSelector:@selector(tnl_delegateQueueForRequestOperation:)] ? [delegate tnl_delegateQueueForRequestOperation:self] : nil; if (!_callbackQueue) { _callbackQueue = _RequestOperationDefaultCallbackQueue(); } // Get the completion queue _completionQueue = [delegate respondsToSelector:@selector(tnl_completionQueueForRequestOperation:)] ? [delegate tnl_completionQueueForRequestOperation:self] : nil; if (!_completionQueue) { _completionQueue = dispatch_get_main_queue(); } _backgroundFlags.didPrep = YES; } } } - (void)_network_start:(BOOL)isRetry { if ([self _network_hasFailedOrFinished]) { // might have been pre-emptively cancelled or failed return; } // Start a background task to keep things running even in the background if (TNLRequestExecutionModeInAppBackgroundTask == _requestConfiguration.executionMode) { [self _network_startBackgroundTask]; // noop in macOS } [self _network_transitionToState:TNLRequestOperationStatePreparingRequest withAttemptResponse:nil]; [self _network_startAttemptTimeoutTimer:_requestConfiguration.attemptTimeout]; // add to queue in case there are existing executions backed up tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ [self _network_prepareToConnectThenConnect:isRetry]; }); } - (void)_network_cleanupAfterComplete { [self _network_invalidateRetry]; [self _network_invalidateOperationTimeoutTimer]; } #pragma mark Private Methods - (void)_network_transitionToState:(TNLRequestOperationState)state withAttemptResponse:(nullable TNLResponse *)attemptResponse { if ([self _network_isStateFinished]) { return; } const TNLRequestOperationState oldState = atomic_load(&_state); if (oldState != state) { if (TNLRequestOperationStateRunning == state && TNLRequestOperationStateStarting != oldState) { return; } if (gTwitterNetworkLayerAssertEnabled) { switch (state) { case TNLRequestOperationStateIdle: TNLAssertNever(); break; case TNLRequestOperationStatePreparingRequest: TNLAssert(TNLRequestOperationStateIdle == oldState || TNLRequestOperationStateWaitingToRetry == oldState); TNLAssert(!attemptResponse); break; case TNLRequestOperationStateStarting: case TNLRequestOperationStateRunning: TNLAssertMessage(oldState < state, @"oldState (%zd) < newState (%zd)", (long) oldState, (long) state); TNLAssert(!attemptResponse); break; case TNLRequestOperationStateWaitingToRetry: TNLAssert(!TNLRequestOperationStateIsFinal(oldState)); TNLAssert(!attemptResponse); break; case TNLRequestOperationStateCancelled: case TNLRequestOperationStateFailed: case TNLRequestOperationStateSucceeded: TNLAssert(!TNLRequestOperationStateIsFinal(oldState)); TNLAssert(attemptResponse != nil); break; } if (oldState == TNLRequestOperationStateWaitingToRetry) { TNLAssert((TNLRequestOperationStateIsFinal(state) || TNLRequestOperationStatePreparingRequest == state)); } } if (TNLRequestOperationStateIsFinal(state)) { // Finished the attempt // we are done with the attempt timer (for now) [self _network_invalidateAttemptTimeoutTimer]; } // either start the retry or complete the state transition [self _network_attemptRetryDuringTransitionFromState:oldState toState:state withAttemptResponse:attemptResponse]; } } - (void)_network_completeTransitionFromState:(TNLRequestOperationState)oldState toState:(TNLRequestOperationState)state withAttemptResponse:(nullable TNLResponse *)attemptResponse { // KVO - Prep BOOL cancelDidChange = NO; BOOL finishedDidChange = NO; BOOL executingDidChange = NO; if (TNLRequestOperationStateIsFinal(state)) { cancelDidChange = (TNLRequestOperationStateCancelled == state); finishedDidChange = YES; executingDidChange = YES; if (!self->_backgroundFlags.didStart) { TNLLogError(@"%@ changed stated to be final before being started!\n%@", NSStringFromClass([self class]), self.hydratedURLRequest); } TNLAssert(_backgroundFlags.didStart); TNLAssert(attemptResponse != nil); } else if (TNLRequestOperationStateIdle == oldState) { executingDidChange = YES; } // Metrics [self _network_updateMetricsFromState:oldState toState:state withAttemptResponse:attemptResponse]; if (attemptResponse) { TNLAttemptCompleteDisposition disposition = TNLAttemptCompleteDispositionCompleting; if (TNLRequestOperationStateWaitingToRetry == state) { disposition = TNLAttemptCompleteDispositionRetrying; } [self _network_didCompleteAttemptWithResponse:attemptResponse disposition:disposition]; } // KVO - Transition if (finishedDidChange) { [self willChangeValueForKey:@"isFinished"]; } if (cancelDidChange) { [self willChangeValueForKey:@"isCancelled"]; } if (executingDidChange) { [self willChangeValueForKey:@"isExecuting"]; } [self setState:state async:NO]; if (executingDidChange) { [self didChangeValueForKey:@"isExecuting"]; } if (cancelDidChange) { [self didChangeValueForKey:@"isCancelled"]; } if (finishedDidChange) { [self didChangeValueForKey:@"isFinished"]; } // Log the transition TNLLogLevel level = TNLLogLevelDebug; if (TNLRequestOperationStateIsFinal(state)) { level = (TNLRequestOperationStateFailed == state) ? TNLLogLevelError : TNLLogLevelInformation; } TNLLog(level, @"%@%@: %@ -> %@\n%@", self, self.URLSessionTaskOperation, TNLRequestOperationStateToString(oldState), TNLRequestOperationStateToString(state), [self _createLogContextStringForState:state withResponse:attemptResponse]); // Delegate callback id<TNLRequestEventHandler> eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:didTransitionFromState:toState:); if ([eventHandler respondsToSelector:callback]) { tnl_dispatch_barrier_async_autoreleasing(_callbackQueue, ^{ NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); [self _updateTag:tag]; [eventHandler tnl_requestOperation:self didTransitionFromState:oldState toState:state]; [self _clearTag:tag]; }); } // Completion if (finishedDidChange) { // have aggressive assert here, whether TNL asserts are enabled or not if (nil == attemptResponse) { __TNLAssertTriggering(); } #if NS_BLOCK_ASSERTIONS assert(attemptResponse != nil); #else TNLCAssert(attemptResponse != nil, @"assertion failed: cannot finish a %@ with a nil TNLResponse!", NSStringFromClass([self class])); #endif [self.requestOperationQueue operation:self didCompleteWithResponse:attemptResponse]; [self _network_completeWithResponse:attemptResponse]; } } - (void)_network_updateMetricsFromState:(TNLRequestOperationState)oldState toState:(TNLRequestOperationState)newState withAttemptResponse:(nullable TNLResponse *)attemptResponse { NSDate *dateNow = [NSDate date]; const uint64_t machTime = mach_absolute_time(); if (TNLRequestOperationStateStarting == newState) { if (!_backgroundFlags.silentStart) { // get the hydrated URL request we will be passing to the NSURLSessionTask in the TNLURLSessionTaskOperation // ... NOT the currentURLRequest since that won't have been applied yet NSURLRequest *request = self.hydratedURLRequest; if (!request) { // we could be going through a transition to an early failure state during/before hydration, // so we'll use some fallbacks to find the best matching request for populating the metrics. // try the incomplete scratch request request = [_scratchURLRequest copy]; if (!request) { // no scratch request, try the hydrated request request = TNLRequestToNSURLRequest(self.hydratedRequest, nil /*config*/, NULL /*errorOut*/); if (!request) { // no hydrated request either, try just the original request request = TNLRequestToNSURLRequest(self.originalRequest, nil /*config*/, NULL /*errorOut*/); } } } TNLAssertMessage(request != nil, @"must have a request by time Starting state happens"); [self willChangeValueForKey:@"attemptCount"]; if (_metrics.attemptCount == 0) { [_metrics addInitialStartWithDate:dateNow machTime:machTime request:request]; [self.requestOperationQueue operation:self didStartAttemptWithMetrics:_metrics.attemptMetrics.lastObject]; } else { // TODO:[nobrien] - if we break apart redirect attempts to own in TNL instead of the NSURLSessionTask, // this will need key off what is causing the state to move to Starting (Retry vs Redirect for example) [self willChangeValueForKey:@"retryCount"]; [_metrics addRetryStartWithDate:dateNow machTime:machTime request:request]; [self.requestOperationQueue operation:self didStartAttemptWithMetrics:_metrics.attemptMetrics.lastObject]; [self didChangeValueForKey:@"retryCount"]; } [self didChangeValueForKey:@"attemptCount"]; } else { TNLAssert(_metrics.attemptCount > 0); } } else if (TNLRequestOperationStateWaitingToRetry == newState || TNLRequestOperationStateIsFinal(newState)) { // We'll have 2 copies of metrics to deal with here // 1) is our running metrics which can keep getting extended // 2) is our TNLResponse for this attempt // Update both copies NSHTTPURLResponse *response = self.currentURLResponse; NSError *error = self.error ?: attemptResponse.operationError; [_metrics addEndDate:dateNow machTime:machTime response:response operationError:error]; TNLResponseMetrics *attemptMetrics = attemptResponse.metrics; [attemptMetrics addEndDate:dateNow machTime:machTime response:response operationError:error]; if (TNLRequestOperationStateIsFinal(newState)) { if (attemptMetrics.completeDate) { [_metrics setCompleteDate:attemptMetrics.completeDate #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" machTime:attemptMetrics.completeMachTime]; #pragma clang diagnostic pop } else { [_metrics setCompleteDate:dateNow machTime:machTime]; [attemptMetrics setCompleteDate:dateNow machTime:machTime]; } } } } - (void)_network_didCompleteAttemptWithResponse:(TNLResponse *)response disposition:(TNLAttemptCompleteDisposition)disposition { if (response) { [self.requestOperationQueue operation:self didCompleteAttempt:response disposition:disposition]; id<TNLRequestEventHandler> eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:didCompleteAttemptWithResponse:disposition:); if ([eventHandler respondsToSelector:callback]) { tnl_dispatch_barrier_async_autoreleasing(_callbackQueue, ^{ NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); [self _updateTag:tag]; [eventHandler tnl_requestOperation:self didCompleteAttemptWithResponse:response disposition:disposition]; [self _clearTag:tag]; }); } } } - (void)_network_completeWithResponse:(TNLResponse *)response { [self _network_prepareToStart]; // ensure we have variables in case we finished before we started [self _network_cleanupAfterComplete]; [self.requestOperationQueue clearQueuedRequestOperation:self]; TNLAssert(nil != response); TNLAssert(_uploadProgress <= 1.0f); TNLAssert(_downloadProgress <= 1.0f); self.internalFinalResponse = response; id<TNLRequestEventHandler> eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:didCompleteWithResponse:); const BOOL hasCompletionCallback = [eventHandler respondsToSelector:callback]; dispatch_block_t block = ^{ if (hasCompletionCallback) { NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); [self _updateTag:tag]; [eventHandler tnl_requestOperation:self didCompleteWithResponse:response]; [self _clearTag:tag]; } [self _finalizeCompletion]; // finalize from the completion queue tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ [self _network_endBackgroundTask]; }); }; if (_callbackQueue == _completionQueue) { tnl_dispatch_barrier_async_autoreleasing(_completionQueue, block); } else { // dispatch to callback queue to flush the callback queue dispatch_barrier_async(_callbackQueue, ^{ // dispatch to completion queue for completion tnl_dispatch_barrier_async_autoreleasing(self->_completionQueue, block); }); } } - (void)_finalizeCompletion { [self willChangeValueForKey:@"isFinished"]; [self willChangeValueForKey:@"isExecuting"]; atomic_store(&_didCompleteFinishedCallback, true); [self didChangeValueForKey:@"isExecuting"]; [self didChangeValueForKey:@"isFinished"]; } - (NSString *)_createLogContextStringForState:(TNLRequestOperationState)state withResponse:(nullable TNLResponse *)response TNL_OBJC_DIRECT { NSMutableDictionary *logContext = [NSMutableDictionary dictionary]; TNLResponseMetrics *metrics = response.metrics; const BOOL logVerboseEnabled = TNLLogVerboseEnabled(); #if DEBUG const BOOL logHeaders = logVerboseEnabled; BOOL logAdvancedInfo = logVerboseEnabled; #else const BOOL logHeaders = (TNLRequestOperationStateFailed == state) && logVerboseEnabled; BOOL logAdvancedInfo = (self.attemptCount > 1 || metrics.totalDuration > 1.0) && logVerboseEnabled; #endif id<TNLRequest> request = self.hydratedRequest ?: self.originalRequest; NSURL *url = [request respondsToSelector:@selector(URL)] ? [(id)request URL] : nil; if (url) { logContext[@"url"] = url; } NSURL *finalURL = response.info.finalURL; if (finalURL && ![finalURL isEqual:url]) { logContext[@"finalURL"] = finalURL; } NSError *error = self.error ?: response.operationError; if (error) { logAdvancedInfo = logVerboseEnabled; NSString *errorDescription = [error description]; if (url) { // errors can often have the url within multiple times; // to reduce verbosity, exclude url errorDescription = [errorDescription stringByReplacingOccurrencesOfString:url.absoluteString withString:@"::url"]; } if (finalURL) { // errors can often have the finalURL within multiple times; // to reduce verbosity, exclude url errorDescription = [errorDescription stringByReplacingOccurrencesOfString:finalURL.absoluteString withString:@"::finalURL"]; } logContext[@"error"] = errorDescription; } if (TNLRequestOperationStateStarting != state) { NSHTTPURLResponse *URLResponse = response.info.URLResponse ?: self.currentURLResponse; if (URLResponse) { logContext[@"statusCode"] = @(URLResponse.statusCode); if (URLResponse.statusCode != TNLHTTPStatusCodeOK) { logAdvancedInfo = logVerboseEnabled; } if (logAdvancedInfo) { NSString *statusCodeString = [NSHTTPURLResponse localizedStringForStatusCode:URLResponse.statusCode]; if (statusCodeString.length > 0) { logContext[@"statusCodeString"] = statusCodeString; } } } } if (logAdvancedInfo) { logContext[@"hydrated"] = @(self.hydratedRequest == request); } if (TNLRequestOperationStateIsFinal(state)) { logContext[@"durationTotal"] = @(metrics.totalDuration); } if (logAdvancedInfo) { if (TNLRequestOperationStateIsActive(state) || TNLRequestOperationStateIsFinal(state)) { logContext[@"countAttempt"] = @(self.attemptCount); logContext[@"countRetry"] = @(self.retryCount); } if (TNLRequestOperationStateIsFinal(state)) { if (TNLRequestOperationStateSucceeded != state) { logContext[@"progressUp"] = @(self.uploadProgress); logContext[@"progressDown"] = @(self.downloadProgress); } else { long long contentLength = [response.info.URLResponse tnl_expectedResponseBodySize]; if (contentLength > 0) { logContext[@"rx-contentLength"] = @(contentLength); } contentLength = [[response.info.finalURLRequest valueForHTTPHeaderField:@"Content-Length"] longLongValue]; if (contentLength > 0) { logContext[@"tx-contentLength"] = @(contentLength); } } NSString *contentEncoding = [response.info.URLResponse tnl_contentEncoding]; if (contentEncoding) { logContext[@"rx-contentEncoding"] = contentEncoding; } contentEncoding = [response.info.finalURLRequest valueForHTTPHeaderField:@"Content-Encoding"]; if (contentEncoding) { logContext[@"tx-contentEncoding"] = contentEncoding; } if (logHeaders) { logContext[@"requestHeaders"] = _redactHeaderFields([response.info.finalURLRequest allHTTPHeaderFields]); logContext[@"responseHeaders"] = _redactHeaderFields([response.info.URLResponse allHeaderFields]); } logContext[@"durationQueued"] = @(metrics.queuedDuration); if ([NSURLSessionConfiguration tnl_URLSessionCanUseTaskTransactionMetrics]) { NSURLSessionTaskTransactionMetrics *taskMetrics = metrics.attemptMetrics.lastObject.taskTransactionMetrics; if (taskMetrics) { NSDictionary *taskMetricsDictionary = [taskMetrics tnl_dictionaryValue]; if (taskMetricsDictionary) { logContext[@"lastTaskMetrics"] = taskMetricsDictionary; } } } } } return [logContext description]; } static NSDictionary<NSString *, NSString *> *_redactHeaderFields(NSDictionary *headerFields) { id<TNLLogger> logger = gTNLLogger; if (!logger) { return [headerFields copy]; } NSMutableDictionary *redactedHeaderFields = [[NSMutableDictionary alloc] init]; [headerFields enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { TNLAssert([key isKindOfClass:[NSString class]]); if ([logger tnl_shouldRedactHTTPHeaderField:key]) { redactedHeaderFields[key] = kRedactedKeyValue; } else { redactedHeaderFields[key] = value; } }]; return [redactedHeaderFields copy]; } - (TNLResponse *)_network_finalizeResponseWithInfo:(TNLResponseInfo *)responseInfo responseError:(nullable NSError *)responseError metadata:(nullable TNLAttemptMetaData *)metadata taskMetrics:(nullable NSURLSessionTaskMetrics *)taskMetrics { [self _network_applyEncodingMetricsToInfo:responseInfo withMetaData:metadata]; [_metrics addMetaData:metadata taskMetrics:taskMetrics]; // Capture any methods we are in when the timeout occurred if ([responseError.domain isEqualToString:TNLErrorDomain]) { switch (responseError.code) { case TNLErrorCodeRequestOperationAttemptTimedOut: case TNLErrorCodeRequestOperationIdleTimedOut: case TNLErrorCodeRequestOperationOperationTimedOut: case TNLErrorCodeRequestOperationCallbackTimedOut: { NSArray *tags = [_callbackTagStack copy]; uint64_t mach_tagTime = _mach_callbackTagTime; if (tags.count > 0) { NSMutableDictionary *userInfo = [responseError.userInfo mutableCopy] ?: [NSMutableDictionary dictionary]; userInfo[TNLErrorTimeoutTagsKey] = tags; userInfo[@"timeoutTagDuration"] = @(TNLAbsoluteToTimeInterval(mach_absolute_time() - mach_tagTime)); userInfo[@"operationId"] = @(self.operationId); responseError = [NSError errorWithDomain:responseError.domain code:responseError.code userInfo:userInfo]; TNLLogError(@"%@", responseError); if (responseError.code == TNLErrorCodeRequestOperationCallbackTimedOut && [TNLGlobalConfiguration sharedInstance].shouldForceCrashOnCloggedCallback) { NSException *exception = [NSException exceptionWithName:@"ForceCrashOnCloggedCallback" reason:@"A callback was clogged!" userInfo:@{ @"error" : responseError }]; #if DEBUG if (TNLIsDebuggerAttached()) { // don't throw an exception when debugging, a breakpoint can easily cause this TNLLogWarning(@"Debugger attached, not throwing exception: %@, %@\n%@", exception, responseError, [NSThread callStackSymbols]); break; } #endif // Crash on the main thread since crashing on this TNL background thread offers no value in stack trace and could be misunderstood tnl_dispatch_async_autoreleasing(dispatch_get_main_queue(), ^{ @throw exception; }); } } break; } default: break; } } // TODO[nobrien]: this is messy... having a response that has to be further manipulated downstream. FIX-ME // Also, this might not the best place to create the response with the defined responseClass TNLResponseMetrics *metrics = [_metrics deepCopyAndTrimIncompleteAttemptMetrics:NO]; TNLResponse *response = [self.responseClass responseWithRequest:_originalRequest operationError:responseError info:responseInfo metrics:metrics]; TNLAssert(response != nil); return response; } - (void)_network_applyEncodingMetricsToInfo:(TNLResponseInfo *)responseInfo withMetaData:(nullable TNLAttemptMetaData *)metadata { if (_scratchURLRequestOriginalBodyLength > 0) { NSString *contentEncoding = [responseInfo.finalURLRequest valueForHTTPHeaderField:@"Content-Encoding"]; if (contentEncoding) { metadata.requestContentLength = _scratchURLRequestEncodedBodyLength; metadata.requestOriginalContentLength = _scratchURLRequestOriginalBodyLength; metadata.requestEncodingLatency = _scratchURLRequestEncodeLatency; } } } #pragma mark Attempt Retry - (BOOL)_network_shouldAttemptRetryDuringTransitionFromState:(TNLRequestOperationState)oldState toState:(TNLRequestOperationState)state withAttemptResponse:(nullable TNLResponse *)attemptResponse { if (!TNLRequestOperationStateIsFinal(state)) { return NO; } TNLAssert(attemptResponse != nil); if (!attemptResponse) { return NO; } if (TNLRequestOperationStateCancelled == state) { return NO; } if ([self _network_hasFailed]) { return NO; } if (TNLRequestOperationStateSucceeded == state) { if (TNLHTTPStatusCodeIsSuccess(attemptResponse.info.statusCode)) { // Can retry on non-defitive HTTP 2xx return !TNLHTTPStatusCodeIsDefinitiveSuccess(attemptResponse.info.statusCode); } } else if ([attemptResponse.operationError.domain isEqualToString:TNLErrorDomain]) { // TNL error encountered... // if we have a callback timeout caused by a delegate/retry-policy callback timing out, we should NOT retry if (attemptResponse.operationError.code == TNLErrorCodeRequestOperationCallbackTimedOut) { return NO; } } return YES; } - (BOOL)_network_shouldForciblyRetryInvalidatedURLSessionRequestWithAttemptResponse:(TNLResponse *)attemptResponse { TNLAssert([attemptResponse.operationError.domain isEqualToString:TNLErrorDomain] && attemptResponse.operationError.code == TNLErrorCodeRequestOperationURLSessionInvalidated); /* The session was invalidated. There is a rare race condition that can have a session invalidate as a new TNLURLSessionTaskOperation starts with that same session. The race condition is within NSURLSession itself and cannot be mitigated. Essentially, it appears as though the NSURL Framework maintains a pool of reuseable NSURLSessionInternal objects that are reused when a session is generated with the same configuration as an existing NSURLSessionInternal object. However, if the internal session will asynchronously be invalidated when its parent NSURLSession is invalidated, the removal of the object from the pool will also be async leaving a window of opportunity for a caller to request a new NSURLSession which will evaluate to an NSURLSessionInternal that is in the pool BUT has already been invalidated so any requests going to that new session will fail due to the session being invalid. This forcible retry will address this issue by identifying symptoms of when this race condition happens. This scenario is most likely to be exacerbated when the delay of a retry policy that kicks in is immediate and there are no other outstanding requests using the same underlying NSURLSession. This is because the retry will immediately go and attempt to use a new NSURLSession, but the underlying NSURLSessionInternal object will be used despite being invalid (or soon to become invalid). An additional measure that is in place is that the minimum delay before a retry is 0.1 seconds. We can safely kick off an immediate retry if: 1) we weren't canceled (can't end up in here if Cancelled == state, so that's given) 2) we didn't get a URL response yet 3) the attempt was shorter than 1 second (which is a generous amount of time) 4) we haven't already retried 4 times */ const unsigned int maxInvalidSessionRetryCount = 4; TNLStaticAssert(maxInvalidSessionRetryCount < 0b1111, Max_Invalid_Session_Retry_Count_must_be_within_4_bits); const NSTimeInterval latestAttemptDuration = TNLComputeDuration( #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" attemptResponse.metrics.currentAttemptStartMachTime, #pragma clang diagnostic pop mach_absolute_time()); const BOOL forciblyRetry = (attemptResponse.info.URLResponse == nil) && (latestAttemptDuration < 1.0) && (_backgroundFlags.invalidSessionRetryCount < maxInvalidSessionRetryCount); TNLLogWarning(@"Encountered a session invalidation error, %@ retrying.\nError: %@", (forciblyRetry) ? @"" : @" not", attemptResponse.operationError); return forciblyRetry; } - (void)_network_forciblyRetryInvalidatedURLSessionRequestWithAttemptResponse:(TNLResponse *)attemptResponse { if (_cachedCancelError) { [self _network_fail:_cachedCancelError]; } else { _backgroundFlags.invalidSessionRetryCount++; self.URLSessionTaskOperation = nil; _backgroundFlags.silentStart = 1; [self _network_transitionToState:TNLRequestOperationStateWaitingToRetry withAttemptResponse:nil]; [self _network_startRetryWithDelay:MIN_TIMER_INTERVAL oldResponse:attemptResponse retryPolicyProvider:nil]; // don't need to end the background task here since we are triggering // the retry in order to circumvent a race condition that causes a failure, // not actually retrying after a legitimate failure that could be of any // duration. } } - (void)_network_attemptRetryDuringTransitionFromState:(TNLRequestOperationState)oldState toState:(TNLRequestOperationState)state withAttemptResponse:(nullable TNLResponse *)attemptResponse { if (_backgroundFlags.inRetryCheck) { return; } const BOOL shouldAttemptRetry = [self _network_shouldAttemptRetryDuringTransitionFromState:oldState toState:state withAttemptResponse:attemptResponse]; if (shouldAttemptRetry && [attemptResponse.operationError.domain isEqualToString:TNLErrorDomain] && attemptResponse.operationError.code == TNLErrorCodeRequestOperationURLSessionInvalidated) { // Invalidated session, we have special logic for this case if ([self _network_shouldForciblyRetryInvalidatedURLSessionRequestWithAttemptResponse:attemptResponse]) { [self _network_forciblyRetryInvalidatedURLSessionRequestWithAttemptResponse:attemptResponse]; return; } } const id<TNLRequestRetryPolicyProvider> retryPolicyProvider = _requestConfiguration.retryPolicyProvider; if (!shouldAttemptRetry || !retryPolicyProvider || ![retryPolicyProvider respondsToSelector:@selector(tnl_shouldRetryRequestOperation:withResponse:)]) { // early check, not going to retry [self _network_completeTransitionFromState:oldState toState:state withAttemptResponse:attemptResponse]; return; } [self _network_retryDuringTransitionFromState:oldState toState:state withAttemptResponse:attemptResponse retryPolicyProvider:retryPolicyProvider]; } - (void)_network_retryDuringTransitionFromState:(TNLRequestOperationState)oldState toState:(TNLRequestOperationState)state withAttemptResponse:(TNLResponse *)attemptResponse retryPolicyProvider:(id<TNLRequestRetryPolicyProvider>)retryPolicyProvider { TNLAssert(retryPolicyProvider != nil); TNLAssert(!_backgroundFlags.inRetryCheck); _backgroundFlags.inRetryCheck = YES; const BOOL hasCachedCancel = _cachedCancelError != nil; const id<TNLRequestEventHandler> eventHandler = self.internalDelegate; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" const uint64_t enqueueMachTime = _metrics.enqueueMachTime; #pragma clang diagnostic pop TNLRequestConfiguration *requestConfig = _requestConfiguration; // Dispatch to the retry queue to get retry policy info tnl_dispatch_barrier_async_autoreleasing(_RetryPolicyProviderQueue(retryPolicyProvider), ^{ NSString *tag = TAG_FROM_METHOD(retryPolicyProvider, @protocol(TNLRequestRetryPolicyProvider), @selector(tnl_shouldRetryRequestOperation:withResponse:)); [self _updateTag:tag]; const BOOL retry = [retryPolicyProvider tnl_shouldRetryRequestOperation:self withResponse:attemptResponse]; [self _clearTag:tag]; if (retry) { NSTimeInterval operationTimeout = requestConfig.operationTimeout; BOOL didUpdateOperationTimeout = NO; TNLRequestConfiguration *newConfig = [self _retryQueue_pullNewRequestConfigurationFromPolicy:retryPolicyProvider attemptResponse:attemptResponse oldConfiguration:requestConfig]; if (newConfig) { TNLLogDebug(@"Retry policy updated config: %@", @{ @"operation" : self, @"attemptResponse" : attemptResponse, @"oldConfig" : requestConfig, @"newConfig" : newConfig }); const NSTimeInterval newTimeout = newConfig.operationTimeout; if (newTimeout != operationTimeout) { operationTimeout = newTimeout; didUpdateOperationTimeout = YES; } } const BOOL hasOperationTimeout = operationTimeout >= MIN_TIMER_INTERVAL; const NSTimeInterval retryDelay = [self _retryQueue_pullRetryDelayFromPolicy:retryPolicyProvider attemptResponse:attemptResponse]; const NSTimeInterval elapsedTime = TNLComputeDuration(enqueueMachTime, mach_absolute_time()); // Only retry if the attempt won't be too far into the future if (!hasOperationTimeout || ((elapsedTime + retryDelay) < operationTimeout)) { TNLLogDebug(@"Retry will start in %.3f seconds", retryDelay); NSTimeInterval newOperationTimeout = -1.0; // negative won't update timeout if (didUpdateOperationTimeout && hasOperationTimeout) { newOperationTimeout = operationTimeout - elapsedTime; TNLAssert(newOperationTimeout >= 0.0); } [self _retryQueue_doRetryWithPolicy:retryPolicyProvider oldState:oldState attemptResponse:attemptResponse retryDelay:retryDelay eventHandler:eventHandler hasCachedCancel:hasCachedCancel newConfiguration:newConfig newOperationTimeout:newOperationTimeout]; return; } TNLLogDebug(@"Retry is past timeout, not retrying"); } // won't retry tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ self->_backgroundFlags.inRetryCheck = NO; if ([self _network_isStateFinished]) { return; } TNLAssert(attemptResponse != nil); [self _network_completeTransitionFromState:oldState toState:state withAttemptResponse:attemptResponse]; }); }); } #pragma mark Retry Static Functions - (NSTimeInterval)_retryQueue_pullRetryDelayFromPolicy:(id<TNLRequestRetryPolicyProvider>)retryPolicyProvider attemptResponse:(TNLResponse *)attemptResponse TNL_OBJC_DIRECT { // get retry delay from retry policy provider const SEL delayCallback = @selector(tnl_delayBeforeRetryForRequestOperation:withResponse:); NSString *tag = TAG_FROM_METHOD(retryPolicyProvider, @protocol(TNLRequestRetryPolicyProvider), delayCallback); [self _updateTag:tag]; NSTimeInterval retryDelay = 0.0; if ([retryPolicyProvider respondsToSelector:delayCallback]) { retryDelay = [retryPolicyProvider tnl_delayBeforeRetryForRequestOperation:self withResponse:attemptResponse]; } [self _clearTag:tag]; if (retryDelay < MIN_TIMER_INTERVAL) { retryDelay = MIN_TIMER_INTERVAL; } return retryDelay; } - (nullable TNLRequestConfiguration *)_retryQueue_pullNewRequestConfigurationFromPolicy:(id<TNLRequestRetryPolicyProvider>)retryPolicyProvider attemptResponse:(TNLResponse *)attemptResponse oldConfiguration:(TNLRequestConfiguration *)oldConfig TNL_OBJC_DIRECT { TNLRequestConfiguration *newConfig = nil; // get new request config from retry policy provider and update _requestConfiguration_ if necessary const SEL newConfigCallback = @selector(tnl_configurationOfRetryForRequestOperation:withResponse:priorConfiguration:); if ([retryPolicyProvider respondsToSelector:newConfigCallback]) { NSString *tag = TAG_FROM_METHOD(retryPolicyProvider, @protocol(TNLRequestRetryPolicyProvider), newConfigCallback); [self _updateTag:tag]; newConfig = [[retryPolicyProvider tnl_configurationOfRetryForRequestOperation:self withResponse:attemptResponse priorConfiguration:oldConfig] copy]; [self _clearTag:tag]; if (newConfig && newConfig == oldConfig) { newConfig = nil; } } return newConfig; } - (void)_retryQueue_doRetryWithPolicy:(id<TNLRequestRetryPolicyProvider>)retryPolicyProvider oldState:(TNLRequestOperationState)oldState attemptResponse:(TNLResponse *)attemptResponse retryDelay:(NSTimeInterval)retryDelay eventHandler:(id<TNLRequestEventHandler>)eventHandler hasCachedCancel:(BOOL)hasCachedCancel newConfiguration:(TNLRequestConfiguration *)newConfig newOperationTimeout:(NSTimeInterval)newOperationTimeout TNL_OBJC_DIRECT { SEL willStartRetryCallback = @selector(tnl_requestOperation:willStartRetryFromResponse:afterDelay:); if (!hasCachedCancel) { if ([retryPolicyProvider respondsToSelector:willStartRetryCallback]) { NSString *tag = TAG_FROM_METHOD(retryPolicyProvider, @protocol(TNLRequestRetryPolicyProvider), willStartRetryCallback); [self _updateTag:tag]; [retryPolicyProvider tnl_requestOperation:self willStartRetryFromResponse:attemptResponse afterDelay:retryDelay]; [self _clearTag:tag]; } } tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ self->_backgroundFlags.inRetryCheck = NO; if ([self _network_hasFailedOrFinished]) { return; } // don't use stale hasCachedCancel var here, // check the fresh _cachedCancel ref directly if (self->_cachedCancelError != nil) { [self _network_fail:self->_cachedCancelError]; return; } if (newConfig) { // update the config self->_requestConfiguration = newConfig; // update the operation timeout if it had changed if (newOperationTimeout > 0) { [self _network_invalidateOperationTimeoutTimer]; [self _network_startOperationTimeoutTimer:newOperationTimeout]; } } self.URLSessionTaskOperation = nil; // Transition to "Waiting to Retry", forcibly updating to "Starting" first, if necessary TNLRequestOperationState updatedOldState = oldState; if (TNLRequestOperationStatePreparingRequest == oldState) { [self _network_completeTransitionFromState:oldState toState:TNLRequestOperationStateStarting withAttemptResponse:nil]; updatedOldState = TNLRequestOperationStateStarting; } [self _network_completeTransitionFromState:updatedOldState toState:TNLRequestOperationStateWaitingToRetry withAttemptResponse:attemptResponse]; // Dispatch to the callback queue in case we need to event to the event handler SEL eventHandlerWillStartRetryCallback = @selector(tnl_requestOperation:willStartRetryFromResponse:policyProvider:afterDelay:); tnl_dispatch_barrier_async_autoreleasing(self->_callbackQueue, ^{ if ([eventHandler respondsToSelector:eventHandlerWillStartRetryCallback] && ((__bridge void *)eventHandler != (__bridge void *)retryPolicyProvider)) { NSString *eventTag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), eventHandlerWillStartRetryCallback); [self _updateTag:eventTag]; [eventHandler tnl_requestOperation:self willStartRetryFromResponse:attemptResponse policyProvider:retryPolicyProvider afterDelay:retryDelay]; [self _clearTag:eventTag]; } // Finish with dispatch to background queue to start retry timer tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ [self _network_startRetryWithDelay:retryDelay oldResponse:attemptResponse retryPolicyProvider:retryPolicyProvider]; // end the background task while waiting to retry, // we only want the active request to be guarded with a bg task [self _network_endBackgroundTask]; }); }); }); } #pragma mark Retry - (void)_network_startRetryWithDelay:(NSTimeInterval)retryDelay oldResponse:(TNLResponse *)oldResponse retryPolicyProvider:(nullable id<TNLRequestRetryPolicyProvider>)retryPolicyProvider { // Update the active retry number (effectively invalidating prior retries) const uint64_t currentRetryId = atomic_fetch_add(&sNextRetryId, 1); _activeRetryId = currentRetryId; // The block try the actual retry __weak typeof(self) weakSelf = self; dispatch_block_t tryRetryBlock = ^{ [weakSelf _network_tryRetryWithId:currentRetryId oldResponse:oldResponse retryPolicyProvider:retryPolicyProvider]; }; // Can we retry without any delay? NSArray<NSOperation *> *dependencies = self.dependencies; if (dependencies.count == 0 && retryDelay < MIN_TIMER_INTERVAL) { // retry without a delay tryRetryBlock(); return; } // Set up operation to gate the retry on NSOperation *retryDependencyOperation = [[TNLSafeOperation alloc] init]; retryDependencyOperation.completionBlock = ^{ if (dispatch_queue_get_label(tnl_network_queue()) == dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)) { tryRetryBlock(); } else { dispatch_async(tnl_network_queue(), tryRetryBlock); } }; // Set up dependencies for (NSOperation *op in dependencies) { if (!op.isFinished && !op.isCancelled) { [retryDependencyOperation addDependency:op]; } } if (retryDelay >= MIN_TIMER_INTERVAL) { // the retry delay is concurrent with the other dependencies, so it doesn't need the other // dependencies itself and simply be added to our dependency operation NSOperation *delayOp = [[TNLTimerOperation alloc] initWithDelay:retryDelay]; [retryDependencyOperation addDependency:delayOp]; [TNLNetworkOperationQueue() addOperation:delayOp]; } // Add our dependency operation to wait for our retry to trigger [TNLNetworkOperationQueue() addOperation:retryDependencyOperation]; } - (void)_network_invalidateRetry { _activeRetryId = 0; } - (void)_network_tryRetryWithId:(uint64_t)retryId oldResponse:(TNLResponse *)oldResponse retryPolicyProvider:(nullable id<TNLRequestRetryPolicyProvider>)retryPolicyProvider { if (retryId == _activeRetryId) { TNLLogInformation(@"%@::_network_tryRetry", self); [self _network_retryWithOldResponse:oldResponse retryPolicyProvider:retryPolicyProvider]; } } #pragma mark Operation Timeout Timer - (void)_network_startOperationTimeoutTimer:(NSTimeInterval)timeInterval { if (!_operationTimeoutTimerSource && timeInterval >= MIN_TIMER_INTERVAL) { __weak typeof(self) weakSelf = self; _operationTimeoutTimerSource = tnl_dispatch_timer_create_and_start(tnl_network_queue(), timeInterval, TIMER_LEEWAY_WITH_FIRE_INTERVAL(timeInterval), NO /*repeats*/, ^{ [weakSelf _network_operationTimeoutTimerDidFire]; }); } } - (void)_network_invalidateOperationTimeoutTimer { tnl_dispatch_timer_invalidate(_operationTimeoutTimerSource); _operationTimeoutTimerSource = NULL; } - (void)_network_operationTimeoutTimerDidFire { if (_operationTimeoutTimerSource) { TNLLogInformation(@"%@::_network_operationTimeoutTimerDidFire", self); [self _network_invalidateOperationTimeoutTimer]; [self _network_invalidateAttemptTimeoutTimer]; [self _network_invalidateRetry]; if (![self _network_hasFailedOrFinished]) { [self _network_fail:TNLErrorCreateWithCode(TNLErrorCodeRequestOperationOperationTimedOut)]; } } } #pragma mark Callback Timeout Timer - (void)_network_startCallbackTimerWithAlreadyElapsedDuration:(NSTimeInterval)alreadyElapsedTime { TNLAssert(!_callbackTimeoutTimerSource); if (_backgroundFlags.isCallbackClogDetectionEnabled) { #if TARGET_OS_IOS || TARGET_OS_TV if (!TNLIsExtension()) { // Lazily prep our app backgrounding observing if (!_backgroundFlags.isObservingApplicationStates) { [self _network_startObservingApplicationStates]; } if (_backgroundFlags.applicationIsInBackground) { // already in the background or is inactive! Set our mach times. _callbackTimeoutTimerStartMachTime = _callbackTimeoutTimerPausedMachTime = mach_absolute_time(); return; } } #endif // IOS + TV __weak typeof(self) weakSelf = self; _callbackTimeoutTimerSource = tnl_dispatch_timer_create_and_start(tnl_network_queue(), _cloggedCallbackTimeout - alreadyElapsedTime, TIMER_LEEWAY_WITH_FIRE_INTERVAL(_cloggedCallbackTimeout), NO /*repeats*/, ^{ [weakSelf _network_callbackTimerFired]; }); _callbackTimeoutTimerStartMachTime = mach_absolute_time() - TNLAbsoluteFromTimeInterval(alreadyElapsedTime); } } - (void)_network_stopCallbackTimer { tnl_dispatch_timer_invalidate(_callbackTimeoutTimerSource); _callbackTimeoutTimerSource = NULL; _callbackTimeoutTimerPausedMachTime = 0; } - (void)_network_startCallbackTimerIfNecessary { if (!_callbackTimeoutTimerSource) { [self _network_startCallbackTimerWithAlreadyElapsedDuration:0.0]; } } - (void)_network_callbackTimerFired { if (_callbackTimeoutTimerSource) { [self _network_stopCallbackTimer]; if (![self _network_hasFailedOrFinished]) { [self _network_fail:TNLErrorCreateWithCode(TNLErrorCodeRequestOperationCallbackTimedOut)]; } } } #if TARGET_OS_IOS || TARGET_OS_TV - (void)_network_pauseCallbackTimer { if (_callbackTimeoutTimerSource) { [self _network_stopCallbackTimer]; _callbackTimeoutTimerPausedMachTime = mach_absolute_time(); } } - (void)_network_unpauseCallbackTimer { if (_callbackTimeoutTimerPausedMachTime) { const NSTimeInterval timeElapsed = TNLComputeDuration(_callbackTimeoutTimerStartMachTime, _callbackTimeoutTimerPausedMachTime); _callbackTimeoutTimerPausedMachTime = 0; [self _network_startCallbackTimerWithAlreadyElapsedDuration:timeElapsed]; } } #endif // IOS + TV #pragma mark Attempt Timeout Timer - (void)_network_startAttemptTimeoutTimer:(NSTimeInterval)timeInterval { if (!_attemptTimeoutTimerSource && timeInterval >= MIN_TIMER_INTERVAL) { __weak typeof(self) weakSelf = self; _attemptTimeoutTimerSource = tnl_dispatch_timer_create_and_start(tnl_network_queue(), timeInterval, TIMER_LEEWAY_WITH_FIRE_INTERVAL(timeInterval), NO /*repeats*/, ^{ [weakSelf _network_attemptTimeoutTimerDidFire]; }); } } - (void)_network_invalidateAttemptTimeoutTimer { tnl_dispatch_timer_invalidate(_attemptTimeoutTimerSource); _attemptTimeoutTimerSource = NULL; } - (void)_network_attemptTimeoutTimerDidFire { if (_attemptTimeoutTimerSource) { TNLLogInformation(@"%@::_network_attemptTimeoutTimerDidFire", self); [self _network_invalidateAttemptTimeoutTimer]; [self _network_invalidateRetry]; // Don't invalidate the operation timeout if (![self _network_hasFailedOrFinished]) { [self _network_fail:TNLErrorCreateWithCode(TNLErrorCodeRequestOperationAttemptTimedOut)]; } } } #pragma mark Background (iOS) - (void)_noop TNL_OBJC_DIRECT { } - (void)_private_willResignActive:(NSNotification *)note { TNLGlobalConfiguration *config = [TNLGlobalConfiguration sharedInstance]; TNLBackgroundTaskIdentifier taskID = [config startBackgroundTaskWithName:@"-[TNLRequestOperation private_willResignActive:]" expirationHandler:^{ // capture self [self _noop]; }]; tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ [self _network_willResignActive]; [config endBackgroundTaskWithIdentifier:taskID]; }); } - (void)_private_didBecomeActive:(NSNotification *)note { tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ [self _network_didBecomeActive]; }); } - (void)_network_willResignActive { #if TARGET_OS_IOS || TARGET_OS_TV _backgroundFlags.applicationIsInBackground = 1; [self _network_pauseCallbackTimer]; #endif } - (void)_network_didBecomeActive { #if TARGET_OS_IOS || TARGET_OS_TV _backgroundFlags.applicationIsInBackground = 0; [self _network_unpauseCallbackTimer]; #endif } #if TARGET_OS_IOS || TARGET_OS_TV - (void)_network_startObservingApplicationStates { TNLAssert(!_backgroundFlags.isObservingApplicationStates); TNLAssert(!TNLIsExtension()); NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc addObserver:self selector:@selector(_private_willResignActive:) name:UIApplicationWillResignActiveNotification object:nil]; [nc addObserver:self selector:@selector(_private_didBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; if ([TNLGlobalConfiguration sharedInstance].lastApplicationState != UIApplicationStateActive) { _backgroundFlags.applicationIsInBackground = 1; } else { _backgroundFlags.applicationIsInBackground = 0; } _backgroundFlags.isObservingApplicationStates = 1; } #endif // IOS + TV #if TARGET_OS_IOS || TARGET_OS_TV - (void)_dealloc_stopObservingApplicationStatesIfNecessary TNL_THREAD_SANITIZER_DISABLED { if (_backgroundFlags.isObservingApplicationStates) { TNLAssert(!TNLIsExtension()); NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc removeObserver:self name:UIApplicationWillResignActiveNotification object:nil]; [nc removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; _backgroundFlags.isObservingApplicationStates = 0; } } #endif // IOS + TV - (void)_network_startBackgroundTask { #if TARGET_OS_IOS || TARGET_OS_TV if (TNLBackgroundTaskInvalid != _backgroundTaskIdentifier) { return; } _backgroundTaskIdentifier = [[TNLGlobalConfiguration sharedInstance] startBackgroundTaskWithName:@"tnl.request.op" expirationHandler:^{ dispatch_sync(tnl_network_queue(), ^{ self->_backgroundTaskIdentifier = TNLBackgroundTaskInvalid; }); }]; #endif // IOS + TV } - (void)_network_endBackgroundTask { #if TARGET_OS_IOS || TARGET_OS_TV if (TNLBackgroundTaskInvalid == _backgroundTaskIdentifier) { return; } [[TNLGlobalConfiguration sharedInstance] endBackgroundTaskWithIdentifier:_backgroundTaskIdentifier]; _backgroundTaskIdentifier = TNLBackgroundTaskInvalid; #endif // IOS + TV } @end #pragma mark - TNLRequestOperation (Convenience) @implementation TNLRequestOperation (Convenience) + (instancetype)operationWithRequest:(nullable id<TNLRequest>)request configuration:(nullable TNLRequestConfiguration *)config delegate:(nullable id<TNLRequestDelegate>)delegate { return [self operationWithRequest:request responseClass:Nil configuration:config delegate:delegate]; } + (instancetype)operationWithURL:(nullable NSURL *)url configuration:(nullable TNLRequestConfiguration *)config delegate:(nullable id<TNLRequestDelegate>)delegate { return [self operationWithRequest:(url) ? [NSURLRequest requestWithURL:url] : nil configuration:config delegate:delegate]; } + (instancetype)operationWithURL:(nullable NSURL *)url completion:(nullable TNLRequestDidCompleteBlock)completion { return [self operationWithRequest:(url) ? [NSURLRequest requestWithURL:url] : nil completion:completion]; } + (instancetype)operationWithRequest:(nullable id<TNLRequest>)request completion:(nullable TNLRequestDidCompleteBlock)completion { return [self operationWithRequest:request configuration:nil completion:completion]; } + (instancetype)operationWithRequest:(nullable id<TNLRequest>)request configuration:(nullable TNLRequestConfiguration *)config completion:(nullable TNLRequestDidCompleteBlock)completion { return [self operationWithRequest:request responseClass:Nil configuration:config completion:completion]; } + (instancetype)operationWithRequest:(nullable id<TNLRequest>)request responseClass:(nullable Class)responseClass configuration:(nullable TNLRequestConfiguration *)config completion:(nullable TNLRequestDidCompleteBlock)completion { TNLSimpleRequestDelegate *delegate = nil; if (completion) { delegate = [[TNLSimpleRequestDelegate alloc] initWithDidCompleteBlock:completion]; } return [self operationWithRequest:request responseClass:responseClass configuration:config delegate:delegate]; } @end @implementation TNLRequestOperation (Tagging) - (void)_updateTag:(NSString *)tag { tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ if (!self->_mach_callbackTagTime) { self->_mach_callbackTagTime = mach_absolute_time(); } [self->_callbackTagStack addObject:tag]; [self _network_startCallbackTimerIfNecessary]; }); } - (void)_clearTag:(NSString *)tag { tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ [self->_callbackTagStack removeObject:tag]; if (self->_callbackTagStack.count == 0) { self->_mach_callbackTagTime = 0; [self _network_stopCallbackTimer]; } }); } @end #pragma mark - TNLTimerOperation @implementation TNLTimerOperation { NSTimeInterval _delay; volatile atomic_bool _finished; volatile atomic_bool _executing; } - (instancetype)initWithDelay:(NSTimeInterval)delay { if (self = [self init]) { _delay = delay; } return self; } - (instancetype)init { if (self = [super init]) { atomic_init(&_finished, false); atomic_init(&_executing, false); } return self; } - (void)start { if ([self isCancelled]) { [self willChangeValueForKey:@"isFinished"]; atomic_store(&_finished, true); [self didChangeValueForKey:@"isFinished"]; return; } [self willChangeValueForKey:@"isExecuting"]; atomic_store(&_executing, true); [self didChangeValueForKey:@"isExecuting"]; [self run]; } - (void)run { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_delay * NSEC_PER_SEC)), tnl_network_queue(), ^{ [self completeOperation]; }); } - (BOOL)isExecuting { return atomic_load(&_executing); } - (BOOL)isFinished { return atomic_load(&_finished); } - (BOOL)isConcurrent { return YES; } - (BOOL)isAsynchronous { return YES; } - (void)completeOperation { [self willChangeValueForKey:@"isFinished"]; [self willChangeValueForKey:@"isExecuting"]; atomic_store(&_executing, false); atomic_store(&_finished, true); [self didChangeValueForKey:@"isExecuting"]; [self didChangeValueForKey:@"isFinished"]; } @end #pragma mark - Functions NSString *TNLRequestOperationStateToString(TNLRequestOperationState state) { #define OP_CASE(c) \ case TNLRequestOperationState##c : { return @"" #c ; } switch (state) { OP_CASE(Idle) // Set by TNLRequestOperation OP_CASE(PreparingRequest) OP_CASE(Starting) // Set by TNLURLSessionTaskOperation OP_CASE(Running) OP_CASE(WaitingToRetry) // Set by TNLRequestOperation or TNLURLSessionTaskOperation OP_CASE(Cancelled) OP_CASE(Failed) OP_CASE(Succeeded) } TNLAssertNever(); return nil; #undef OP_CASE } NS_ASSUME_NONNULL_END