/* Copyright 2014 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif

#import "GTMSessionFetcher/GTMSessionFetcher.h"
#import "GTMSessionFetcher/GTMSessionFetcherService.h"
#import "GTMSessionFetcherService+Internal.h"

#if TARGET_OS_OSX && GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH
// To reconnect background sessions on Mac outside +load requires importing and linking
// AppKit to access the NSApplicationDidFinishLaunching symbol.
#import <AppKit/AppKit.h>
#endif

#include <sys/utsname.h>

#ifndef STRIP_GTM_FETCH_LOGGING
#error GTMSessionFetcher headers should have defaulted this if it wasn't already defined.
#endif

NS_ASSUME_NONNULL_BEGIN

NSString *const kGTMSessionFetcherStartedNotification = @"kGTMSessionFetcherStartedNotification";
NSString *const kGTMSessionFetcherStoppedNotification = @"kGTMSessionFetcherStoppedNotification";
NSString *const kGTMSessionFetcherRetryDelayStartedNotification =
    @"kGTMSessionFetcherRetryDelayStartedNotification";
NSString *const kGTMSessionFetcherRetryDelayStoppedNotification =
    @"kGTMSessionFetcherRetryDelayStoppedNotification";

NSString *const kGTMSessionFetcherCompletionInvokedNotification =
    @"kGTMSessionFetcherCompletionInvokedNotification";
NSString *const kGTMSessionFetcherCompletionDataKey = @"data";
NSString *const kGTMSessionFetcherCompletionErrorKey = @"error";

NSString *const kGTMSessionFetcherErrorDomain = @"com.google.GTMSessionFetcher";
NSString *const kGTMSessionFetcherStatusDomain = @"com.google.HTTPStatus";
NSString *const kGTMSessionFetcherStatusDataKey =
    @"data";  // data returned with a kGTMSessionFetcherStatusDomain error
NSString *const kGTMSessionFetcherStatusDataContentTypeKey = @"data_content_type";

NSString *const kGTMSessionFetcherNumberOfRetriesDoneKey =
    @"kGTMSessionFetcherNumberOfRetriesDoneKey";
NSString *const kGTMSessionFetcherElapsedIntervalWithRetriesKey =
    @"kGTMSessionFetcherElapsedIntervalWithRetriesKey";

static NSString *const kGTMSessionIdentifierPrefix = @"com.google.GTMSessionFetcher";
static NSString *const kGTMSessionIdentifierDestinationFileURLMetadataKey = @"_destURL";
static NSString *const kGTMSessionIdentifierBodyFileURLMetadataKey = @"_bodyURL";
static NSString *const kGTMSessionIdentifierClientReconnectMetadataKey = @"_clientWillReconnect";

// The default max retry interview is 10 minutes for uploads (POST/PUT/PATCH),
// 1 minute for downloads.
static const NSTimeInterval kUnsetMaxRetryInterval = -1.0;
static const NSTimeInterval kDefaultMaxDownloadRetryInterval = 60.0;
static const NSTimeInterval kDefaultMaxUploadRetryInterval = 60.0 * 10.;

// The maximum data length that can be loaded to the error userInfo
static const int64_t kMaximumDownloadErrorDataLength = 20000;

#ifdef GTMSESSION_PERSISTED_DESTINATION_KEY
// Projects using unique class names should also define a unique persisted destination key.
static NSString *const kGTMSessionFetcherPersistedDestinationKey =
    GTMSESSION_PERSISTED_DESTINATION_KEY;
#else
static NSString *const kGTMSessionFetcherPersistedDestinationKey =
    @"com.google.GTMSessionFetcher.downloads";
#endif

NS_ASSUME_NONNULL_END

//
// GTMSessionFetcher
//

#if 0
#define GTM_LOG_BACKGROUND_SESSION(...) GTMSESSION_LOG_DEBUG(__VA_ARGS__)
#else
#define GTM_LOG_BACKGROUND_SESSION(...)
#endif

#ifndef GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY
#define GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY 1
#endif

#if __has_attribute(swift_async)
// Once Clang 13/Xcode 13 can be assumed, can switch to NS_SWIFT_DISABLE_ASYNC.
#define GTM_SWIFT_DISABLE_ASYNC __attribute__((swift_async(none)))
#else
#define GTM_SWIFT_DISABLE_ASYNC
#endif

// Internal tracking of the state within the `-beginFetch...` flow.
typedef NS_ENUM(NSUInteger, GTMSessionFetcherStartingState) {
  // Not in any explcit part of the startup flow or about the re-enter the flow.
  kStartingStateNone = 0,

  // Parts of startup that can result in the fetcher completing startup at some later time.
  kStartingStateServiceDelayed,
  kStartingStateCalculatingUA,
  kStartingStateAuthorizing,
  kStartingStateApplyingDecorators,

  // State while actively in `-beginFetchMayDelay:mayAuthorize:mayDecorate:`.
  kStartingStateStartingUp,
};

@interface GTMSessionFetcher ()

@property(atomic, strong, readwrite, nullable) NSData *downloadedData;
@property(atomic, strong, readwrite, nullable) NSData *downloadResumeData;

#if GTM_BACKGROUND_TASK_FETCHING
// Should always be accessed within an @synchronized(self).
@property(assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskIdentifier;
#endif

@property(atomic, readwrite, getter=isUsingBackgroundSession) BOOL usingBackgroundSession;

@end

#if !GTMSESSION_BUILD_COMBINED_SOURCES
@interface GTMSessionFetcher (GTMSessionFetcherLoggingInternal)
- (void)logFetchWithError:(NSError *)error;
- (void)logNowWithError:(nullable NSError *)error;
- (NSInputStream *)loggedInputStreamForInputStream:(NSInputStream *)inputStream;
- (GTMSessionFetcherBodyStreamProvider)loggedStreamProviderForStreamProvider:
    (GTMSessionFetcherBodyStreamProvider)streamProvider;
@end
#endif  // !GTMSESSION_BUILD_COMBINED_SOURCES

NS_ASSUME_NONNULL_BEGIN

static NSTimeInterval InitialMinRetryInterval(void) {
  return 1.0 + ((double)(arc4random_uniform(0x0FFFF)) / (double)0x0FFFF);
}

static BOOL IsLocalhost(NSString *_Nullable host) {
  // We check if there's host, and then make the comparisons.
  if (host == nil) return NO;
  return ([host caseInsensitiveCompare:@"localhost"] == NSOrderedSame || [host isEqual:@"::1"] ||
          [host isEqual:@"127.0.0.1"]);
}

static NSDictionary *_Nullable GTMErrorUserInfoForData(NSData *_Nullable data,
                                                       NSDictionary *_Nullable responseHeaders) {
  NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];

  if (data.length > 0) {
    userInfo[kGTMSessionFetcherStatusDataKey] = data;

    NSString *contentType = responseHeaders[@"Content-Type"];
    if (contentType) {
      userInfo[kGTMSessionFetcherStatusDataContentTypeKey] = contentType;
    }
  }

  return userInfo.count > 0 ? userInfo : nil;
}

static GTMSessionFetcherTestBlock _Nullable gGlobalTestBlock;

@implementation GTMSessionFetcher {
  NSMutableURLRequest *_request;  // after beginFetch, changed only in delegate callbacks
  BOOL _useUploadTask;            // immutable after beginFetch
  NSURL *_bodyFileURL;            // immutable after beginFetch
  GTMSessionFetcherBodyStreamProvider _bodyStreamProvider;  // immutable after beginFetch
  NSURLSession *_session;
  BOOL _shouldInvalidateSession;  // immutable after beginFetch
  NSURLSession *_sessionNeedingInvalidation;
  NSURLSessionConfiguration *_configuration;
  NSURLSessionTask *_sessionTask;
  NSString *_taskDescription;
  float _taskPriority;
  NSURLResponse *_response;
  NSString *_sessionIdentifier;
  BOOL _wasCreatedFromBackgroundSession;
  BOOL _clientWillReconnectBackgroundSession;
  BOOL _didCreateSessionIdentifier;
  NSString *_sessionIdentifierUUID;
  BOOL _userRequestedBackgroundSession;
  BOOL _usingBackgroundSession;
  NSMutableData *_Nullable _downloadedData;
  NSError *_downloadFinishedError;
  NSData *_downloadResumeData;               // immutable after construction
  NSData *_Nullable _downloadTaskErrorData;  // Data for when download task fails
  NSURL *_destinationFileURL;
  int64_t _downloadedLength;
  NSURLCredential *_credential;       // username & password
  NSURLCredential *_proxyCredential;  // credential supplied to proxy servers
  BOOL _isStopNotificationNeeded;     // set when start notification has been sent
  BOOL _isUsingTestBlock;  // set when a test block was provided (remains set when the block is
                           // released)
  id _userData;            // retained, if set by caller
  NSMutableDictionary *_properties;  // more data retained for caller
  dispatch_queue_t _callbackQueue;
  dispatch_group_t _callbackGroup;   // read-only after creation
  NSOperationQueue *_delegateQueue;  // immutable after beginFetch

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
  id<GTMFetcherAuthorizationProtocol> _authorizer;  // immutable after beginFetch
#pragma clang diagnostic pop

  // The service object that created and monitors this fetcher, if any.
  GTMSessionFetcherService *_service;  // immutable; set by the fetcher service upon creation
  NSString *_serviceHost;
  NSInteger _servicePriority;  // immutable after beginFetch
  BOOL _hasStoppedFetching;    // counterpart to _initialBeginFetchDate
  BOOL _userStoppedFetching;

  BOOL _isRetryEnabled;  // user wants auto-retry
  NSTimer *_retryTimer;
  NSUInteger _retryCount;
  NSTimeInterval _maxRetryInterval;  // default 60 (download) or 600 (upload) seconds
  NSTimeInterval _minRetryInterval;  // random between 1 and 2 seconds
  NSTimeInterval _retryFactor;       // default interval multiplier is 2
  NSTimeInterval _lastRetryInterval;
  NSDate *_initialBeginFetchDate;  // date that beginFetch was first invoked; immutable after
                                   // initial beginFetch
  NSDate *_initialRequestDate;     // date of first request to the target server (ignoring auth)
  BOOL _hasAttemptedAuthRefresh;   // accessed only in shouldRetryNowForStatus:

  GTMSessionFetcherStartingState _startingState;
  NSUInteger _pendingNotifications;

  NSString *_comment;  // comment for log
  NSString *_log;
#if !STRIP_GTM_FETCH_LOGGING
  NSMutableData *_loggedStreamData;
  NSURL *_redirectedFromURL;
  NSString *_logRequestBody;
  NSString *_logResponseBody;
  BOOL _hasLoggedError;
  BOOL _deferResponseBodyLogging;
#endif
}

#if !GTMSESSION_UNIT_TESTING
+ (void)load {
#if GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH && TARGET_OS_IPHONE
  NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
  [nc addObserver:self
         selector:@selector(reconnectFetchersForBackgroundSessionsOnAppLaunch:)
             name:UIApplicationDidFinishLaunchingNotification
           object:nil];
#elif GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH && TARGET_OS_OSX
  NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
  [nc addObserver:self
         selector:@selector(reconnectFetchersForBackgroundSessionsOnAppLaunch:)
             name:NSApplicationDidFinishLaunchingNotification
           object:nil];
#else
  [self fetchersForBackgroundSessions];
#endif
}

+ (void)reconnectFetchersForBackgroundSessionsOnAppLaunch:(NSNotification *)notification {
  // Give all other app-did-launch handlers a chance to complete before
  // reconnecting the fetchers. Not doing this may lead to reconnecting
  // before the app delegate has a chance to run.
  dispatch_async(dispatch_get_main_queue(), ^{
    [self fetchersForBackgroundSessions];
  });
}
#endif  // !GTMSESSION_UNIT_TESTING

+ (instancetype)fetcherWithRequest:(nullable NSURLRequest *)request {
  return [[self alloc] initWithRequest:request configuration:nil];
}

+ (instancetype)fetcherWithURL:(NSURL *)requestURL {
  return [self fetcherWithRequest:[NSURLRequest requestWithURL:requestURL]];
}

+ (instancetype)fetcherWithURLString:(NSString *)requestURLString {
  return [self fetcherWithURL:(NSURL *)[NSURL URLWithString:requestURLString]];
}

+ (instancetype)fetcherWithDownloadResumeData:(NSData *)resumeData {
  GTMSessionFetcher *fetcher = [self fetcherWithRequest:nil];
  fetcher.comment = @"Resuming download";
  fetcher.downloadResumeData = resumeData;
  return fetcher;
}

+ (nullable instancetype)fetcherWithSessionIdentifier:(NSString *)sessionIdentifier {
  GTMSESSION_ASSERT_DEBUG(sessionIdentifier != nil, @"Invalid session identifier");
  NSMapTable *sessionIdentifierToFetcherMap = [self sessionIdentifierToFetcherMap];
  GTMSessionFetcher *fetcher = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier];
  if (!fetcher && [sessionIdentifier hasPrefix:kGTMSessionIdentifierPrefix]) {
    fetcher = [self fetcherWithRequest:nil];
    [fetcher setSessionIdentifier:sessionIdentifier];
    [sessionIdentifierToFetcherMap setObject:fetcher forKey:sessionIdentifier];
    fetcher->_wasCreatedFromBackgroundSession = YES;
    [fetcher setCommentWithFormat:@"Resuming %@", fetcher && fetcher->_sessionIdentifierUUID
                                                      ? fetcher->_sessionIdentifierUUID
                                                      : @"?"];
  }
  return fetcher;
}

+ (NSMapTable *)sessionIdentifierToFetcherMap {
  // TODO: What if a service is involved in creating the fetcher? Currently, when re-creating
  // fetchers, if a service was involved, it is not re-created. Should the service maintain a map?
  static NSMapTable *gSessionIdentifierToFetcherMap = nil;

  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    gSessionIdentifierToFetcherMap = [NSMapTable strongToWeakObjectsMapTable];
  });
  return gSessionIdentifierToFetcherMap;
}

#if !GTM_ALLOW_INSECURE_REQUESTS
+ (BOOL)appAllowsInsecureRequests {
  // If the main bundle Info.plist key NSAppTransportSecurity is present, and it specifies
  // NSAllowsArbitraryLoads, then we need to explicitly enforce secure schemes.
#if GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY
  static BOOL allowsInsecureRequests;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    NSBundle *mainBundle = [NSBundle mainBundle];
    NSDictionary *appTransportSecurity =
        [mainBundle objectForInfoDictionaryKey:@"NSAppTransportSecurity"];
    allowsInsecureRequests =
        [[appTransportSecurity objectForKey:@"NSAllowsArbitraryLoads"] boolValue];
  });
  return allowsInsecureRequests;
#else
  // For builds targeting iOS 8 or 10.10 and earlier, we want to require fetcher
  // security checks.
  return YES;
#endif  // GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY
}
#else   // GTM_ALLOW_INSECURE_REQUESTS
+ (BOOL)appAllowsInsecureRequests {
  return YES;
}
#endif  // !GTM_ALLOW_INSECURE_REQUESTS

- (instancetype)init {
  return [self initWithRequest:nil configuration:nil];
}

- (instancetype)initWithRequest:(NSURLRequest *)request {
  return [self initWithRequest:request configuration:nil];
}

- (instancetype)initWithRequest:(nullable NSURLRequest *)request
                  configuration:(nullable NSURLSessionConfiguration *)configuration {
  self = [super init];
  if (self) {
#if GTM_BACKGROUND_TASK_FETCHING
    _backgroundTaskIdentifier = UIBackgroundTaskInvalid;
#endif
    _request = [request mutableCopy];
    _configuration = configuration;

    NSData *bodyData = request.HTTPBody;
    if (bodyData) {
      _bodyLength = (int64_t)bodyData.length;
    } else {
      _bodyLength = NSURLSessionTransferSizeUnknown;
    }

    _callbackQueue = dispatch_get_main_queue();
    _callbackGroup = dispatch_group_create();
    _delegateQueue = [NSOperationQueue mainQueue];

    _minRetryInterval = InitialMinRetryInterval();
    _maxRetryInterval = kUnsetMaxRetryInterval;

    _taskPriority = -1.0f;  // Valid values if set are 0.0...1.0.

    _testBlockAccumulateDataChunkCount = 1;

#if !STRIP_GTM_FETCH_LOGGING
    // Encourage developers to set the comment property or use
    // setCommentWithFormat: by providing a default string.
    _comment = @"(No fetcher comment set)";
#endif
  }
  return self;
}

- (id)copyWithZone:(NSZone *)zone {
  // disallow use of fetchers in a copy property
  [self doesNotRecognizeSelector:_cmd];
  return nil;
}

- (NSString *)description {
  NSString *requestStr = self.request.URL.description;
  if (requestStr.length == 0) {
    if (self.downloadResumeData.length > 0) {
      requestStr = @"<download resume data>";
    } else if (_wasCreatedFromBackgroundSession) {
      requestStr = @"<from bg session>";
    } else {
      requestStr = @"<no request>";
    }
  }
  return [NSString stringWithFormat:@"%@ %p (%@)", [self class], self, requestStr];
}

- (void)dealloc {
  GTMSESSION_ASSERT_DEBUG(!_isStopNotificationNeeded, @"unbalanced fetcher notification for %@",
                          _request.URL);
  [self forgetSessionIdentifierForFetcherWithoutSyncCheck];

  // Note: if a session task or a retry timer was pending, then this instance
  // would be retained by those so it wouldn't be getting dealloc'd,
  // hence we don't need to stopFetch here
}

#pragma mark -

// Begin fetching the URL (or begin a retry fetch).  The delegate is retained
// for the duration of the fetch connection.

- (void)beginFetchWithCompletionHandler:(nullable GTMSessionFetcherCompletionHandler)handler {
  GTMSessionCheckNotSynchronized(self);
  _completionHandler = [handler copy];

  // The user may have called setDelegate: earlier if they want to use other
  // delegate-style callbacks during the fetch; otherwise, the delegate is nil,
  // which is fine.
  [self beginFetchMayDelay:YES mayAuthorize:YES mayDecorate:YES];
}

// Begin fetching the URL for a retry fetch. The delegate and completion handler
// are already provided, and do not need to be copied.
- (void)beginFetchForRetry {
  GTMSessionCheckNotSynchronized(self);
  [self beginFetchMayDelay:YES mayAuthorize:YES mayDecorate:YES];
}

- (GTMSessionFetcherCompletionHandler)completionHandlerWithTarget:(nullable id)target
                                                didFinishSelector:(nullable SEL)finishedSelector {
  GTMSessionFetcherAssertValidSelector(target, finishedSelector, @encode(GTMSessionFetcher *),
                                       @encode(NSData *), @encode(NSError *), 0);
  GTMSessionFetcherCompletionHandler completionHandler = ^(NSData *data, NSError *error) {
    if (target && finishedSelector) {
      id selfArg = self;  // Placate ARC.
      NSMethodSignature *sig = [target methodSignatureForSelector:finishedSelector];
      NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
      [invocation setSelector:(SEL)finishedSelector];
      [invocation setTarget:target];
      [invocation setArgument:&selfArg atIndex:2];
      [invocation setArgument:&data atIndex:3];
      [invocation setArgument:&error atIndex:4];
      [invocation invoke];
    }
  };
  return completionHandler;
}

- (void)beginFetchWithDelegate:(nullable id)target
             didFinishSelector:(nullable SEL)finishedSelector {
  GTMSessionCheckNotSynchronized(self);

  GTMSessionFetcherCompletionHandler handler = [self completionHandlerWithTarget:target
                                                               didFinishSelector:finishedSelector];
  [self beginFetchWithCompletionHandler:handler];
}

// Helper to enter a new StartingState. If the fetch has already been stopped, then it will trigger
// a needed callback instead of setting the state. It returns YES/NO based on if starting up of the
// fetch should continue.
- (BOOL)startingState:(GTMSessionFetcherStartingState)newState __attribute__((objc_direct)) {
  BOOL stopped = NO;
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    GTMSESSION_ASSERT_DEBUG(
        (_startingState == kStartingStateNone || _startingState == kStartingStateStartingUp),
        @"Unexpected starting state: %lu", (unsigned long)_startingState);

    if (_userStoppedFetching) {
      stopped = YES;
    } else {
      _startingState = newState;
    }
  }
  if (stopped) {
    // We end up here if someone calls `stopFetching` from another thread/queue while
    // the fetch was being started up, so while `stopFetching` did the needed shutdown
    // we have to ensure the requested callback was triggered.
    if (self.stopFetchingTriggersCompletionHandler) {
      NSError *error = [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
                                           code:GTMSessionFetcherErrorUserCancelled
                                       userInfo:nil];
      [self failToBeginFetchWithError:error];
    } else {
      // In the edge case where the fetch was stopped before it even began, the call backs still
      // need to get cleared as at least the completion will have been just set in calling
      // beginFetch...
      [self stopFetchReleasingCallbacks:YES];
    }
    return NO;  // Caller to stop.
  }
  return YES;
}

// This is a private callback from the service to restart the fetcher after it was delayed
// due to the per host throttling.
- (void)serviceRestartingFetcher {
  // Reset the starting state since the fetch is getting restarted.
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);
    _startingState = kStartingStateNone;
  }
  [self beginFetchMayDelay:NO mayAuthorize:YES mayDecorate:YES];
}

- (void)beginFetchMayDelay:(BOOL)mayDelay
              mayAuthorize:(BOOL)mayAuthorize
               mayDecorate:(BOOL)mayDecorate {
  // This is the internal entry point for re-starting fetches.
  GTMSessionCheckNotSynchronized(self);

  if (![self startingState:kStartingStateStartingUp]) {
    return;
  }

  // The request property is now externally immutable.
  NSMutableURLRequest *fetchRequest = _request;

  NSURL *fetchRequestURL = fetchRequest.URL;
  NSString *priorSessionIdentifier = self.sessionIdentifier;

  GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URL:%@ beginFetchMayDelay:%d mayAuthorize:%d mayDecorate:%d",
                               [self class], self, _request.URL, mayDelay, mayAuthorize,
                               mayDecorate);

  // A utility block for creating error objects when we fail to start the fetch.
  NSError * (^beginFailureError)(NSInteger) = ^(NSInteger code) {
    NSString *urlString = fetchRequestURL.absoluteString;
    NSDictionary *userInfo =
        @{NSURLErrorFailingURLStringErrorKey : (urlString ? urlString : @"(missing URL)")};
    return [NSError errorWithDomain:kGTMSessionFetcherErrorDomain code:code userInfo:userInfo];
  };

  // Catch delegate queue maxConcurrentOperationCount values other than 1, particularly
  // NSOperationQueueDefaultMaxConcurrentOperationCount (-1), to avoid the additional complexity
  // of simultaneous or out-of-order delegate callbacks.
  GTMSESSION_ASSERT_DEBUG(_delegateQueue.maxConcurrentOperationCount == 1,
                          @"delegate queue %@ should support one concurrent operation, not %ld",
                          _delegateQueue.name, (long)_delegateQueue.maxConcurrentOperationCount);

  if (!_initialBeginFetchDate) {
    // This ivar is set only here on the initial beginFetch so need not be synchronized.
    _initialBeginFetchDate = [[NSDate alloc] init];
  }

  if (self.sessionTask != nil) {
    // If cached fetcher returned through fetcherWithSessionIdentifier:, then it's
    // already begun, but don't consider this a failure, since the user need not know this.
    if (self.sessionIdentifier != nil) {
      return;
    }
    GTMSESSION_ASSERT_DEBUG(NO, @"Fetch object %@ being reused; this should never happen", self);
    [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorDownloadFailed)];
    return;
  }

  if (fetchRequestURL == nil && !_downloadResumeData && !priorSessionIdentifier) {
    GTMSESSION_ASSERT_DEBUG(NO, @"Beginning a fetch requires a request with a URL");
    [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorDownloadFailed)];
    return;
  }

  // We'll respect the user's request for a background session (unless this is
  // an upload fetcher, which does its initial request foreground.)
  self.usingBackgroundSession = self.useBackgroundSession && [self canFetchWithBackgroundSession];

  NSURL *bodyFileURL = self.bodyFileURL;
  if (bodyFileURL) {
    NSError *fileCheckError;
    if (![bodyFileURL checkResourceIsReachableAndReturnError:&fileCheckError]) {
      // This assert fires when the file being uploaded no longer exists once
      // the fetcher is ready to start the upload.
      GTMSESSION_ASSERT_DEBUG_OR_LOG(0, @"Body file is unreachable: %@\n  %@", bodyFileURL.path,
                                     fileCheckError);
      [self failToBeginFetchWithError:fileCheckError];
      return;
    }
  }

  NSString *requestScheme = fetchRequestURL.scheme;
  BOOL isDataRequest = [requestScheme isEqual:@"data"];
  if (isDataRequest) {
    // NSURLSession does not support data URLs in background sessions.
#if DEBUG
    if (priorSessionIdentifier || self.sessionIdentifier) {
      GTMSESSION_LOG_DEBUG(@"Converting background to foreground session for %@", fetchRequest);
    }
#endif
    // If priorSessionIdentifier is allowed to stay non-nil, a background session can
    // still be created.
    priorSessionIdentifier = nil;
    [self setSessionIdentifierInternal:nil];
    self.usingBackgroundSession = NO;
  }

#if GTM_ALLOW_INSECURE_REQUESTS
  BOOL shouldCheckSecurity = NO;
#else
  BOOL shouldCheckSecurity =
      (fetchRequestURL != nil && !isDataRequest && [[self class] appAllowsInsecureRequests]);
#endif

  if (shouldCheckSecurity) {
    // Allow https only for requests, unless overridden by the client.
    //
    // Non-https requests may too easily be snooped, so we disallow them by default.
    //
    // file: and data: schemes are usually safe if they are hardcoded in the client or provided
    // by a trusted source, but since it's fairly rare to need them, it's safest to make clients
    // explicitly allow them.
    BOOL isSecure =
        requestScheme != nil && [requestScheme caseInsensitiveCompare:@"https"] == NSOrderedSame;
    if (!isSecure) {
      BOOL allowRequest = NO;
      NSString *host = fetchRequestURL.host;

      // Check schemes first.  A file scheme request may be allowed here, or as a localhost request.
      for (NSString *allowedScheme in _allowedInsecureSchemes) {
        if (requestScheme != nil &&
            [requestScheme caseInsensitiveCompare:allowedScheme] == NSOrderedSame) {
          allowRequest = YES;
          break;
        }
      }
      if (!allowRequest) {
        // Check for localhost requests.  Security checks only occur for non-https requests, so
        // this check won't happen for an https request to localhost.
        BOOL isLocalhostRequest =
            (host.length == 0 && [fetchRequestURL isFileURL]) || IsLocalhost(host);
        if (isLocalhostRequest) {
          if (self.allowLocalhostRequest) {
            allowRequest = YES;
          } else {
            GTMSESSION_ASSERT_DEBUG(NO,
                                    @"Fetch request for localhost but fetcher"
                                    @" allowLocalhostRequest is not set: %@",
                                    fetchRequestURL);
          }
        } else {
          GTMSESSION_ASSERT_DEBUG(NO,
                                  @"Insecure fetch request has a scheme (%@)"
                                  @" not found in fetcher allowedInsecureSchemes (%@): %@",
                                  requestScheme, _allowedInsecureSchemes ?: @" @[] ",
                                  fetchRequestURL);
        }
      }

      if (!allowRequest) {
#if !DEBUG
        NSLog(@"Insecure fetch disallowed for %@",
              fetchRequestURL.description ?: @"nil request URL");
#endif
        [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorInsecureRequest)];
        return;
      }
    }  // !isSecure
  }  // (requestURL != nil) && !isDataRequest

  if (self.cookieStorage == nil) {
    self.cookieStorage = [[self class] staticCookieStorage];
  }

  BOOL isRecreatingSession = (self.sessionIdentifier != nil) && (fetchRequest == nil);

  self.canShareSession = (_service != nil) && !isRecreatingSession && !self.usingBackgroundSession;

  if (!self.session) {
    if (self.canShareSession) {
      self.session = [_service
          sessionWithCreationBlock:^NSURLSession *(id<NSURLSessionDelegate> sessionDelegate) {
            return [self createSessionWithDelegate:sessionDelegate
                                 sessionIdentifier:priorSessionIdentifier];
          }];
    } else {
      self.session = [self createSessionWithDelegate:self sessionIdentifier:priorSessionIdentifier];
    }
  }

  if (isRecreatingSession) {
    _shouldInvalidateSession = YES;

    // Let's make sure there are tasks still running or if not that we get a callback from a
    // completed one; otherwise, we assume the tasks failed.
    // This is the observed behavior perhaps 25% of the time within the Simulator running 7.0.3 on
    // exiting the app after starting an upload and relaunching the app if we manage to relaunch
    // after the task has completed, but before the system relaunches us in the background.
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks,
                                                  NSArray *downloadTasks) {
      if (dataTasks.count == 0 && uploadTasks.count == 0 && downloadTasks.count == 0) {
        double const kDelayInSeconds = 1.0;  // We should get progress indication or completion soon
        dispatch_time_t checkForFeedbackDelay =
            dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kDelayInSeconds * NSEC_PER_SEC));
        dispatch_after(checkForFeedbackDelay, dispatch_get_main_queue(), ^{
          if (!self.sessionTask && !fetchRequest) {
            // If our task and/or request haven't been restored, then we assume task feedback lost.
            [self removePersistedBackgroundSessionFromDefaults];
            NSError *sessionError =
                [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
                                    code:GTMSessionFetcherErrorBackgroundFetchFailed
                                userInfo:nil];
            [self failToBeginFetchWithError:sessionError];
          }
        });
      }
    }];
    return;
  }

  self.downloadedData = nil;
  self.downloadedLength = 0;

  if (_servicePriority == NSIntegerMin) {
    mayDelay = NO;
  }
  if (mayDelay && _service) {
    // Set the delayed state so there can't be a race between it getting queued in the
    // `fetcherShouldBeginFetching:` call and some other thread completing a different fetch and
    // thus starting this one. If we were trying to set the state based on the return result, there
    // would be a small window for that race.
    if (![self startingState:kStartingStateServiceDelayed]) {
      return;
    }

    BOOL shouldFetchNow = [_service fetcherShouldBeginFetching:self];
    if (!shouldFetchNow) {
      // The fetch is deferred, but will happen later.
      //
      // If this session is held by the fetcher service, clear the session now so that we don't
      // assume it's still valid after the fetcher is restarted.
      //
      // NOTE: In hindsight, this could be a race, some other thread could be starting it while
      // doing these checks/work, but it also doesn't seem safe to put the whole block in a
      // since `@synchronized(self)` block.
      if (self.canShareSession) {
        self.session = nil;
      }
      return;
    }

    @synchronized(self) {
      GTMSessionMonitorSynchronized(self);

      // If a `-stopFetching` came in while the service check was made, then the side effect of the
      // state setting caused the handler (if needed) to already be made, so there we want to just
      // exit and not continue the fetch.
      if (_userStoppedFetching) {
        return;
      }

      // Per comment above, correct state since it wasn't delayed, go back to startup state.
      _startingState = kStartingStateStartingUp;
    }
  }

  if ([fetchRequest valueForHTTPHeaderField:@"User-Agent"] == nil) {
    id<GTMUserAgentProvider> userAgentProvider = _userAgentProvider;
    NSString *cachedUserAgent = userAgentProvider.cachedUserAgent;
    if (cachedUserAgent) {
      // The User-Agent is already cached in memory, so set it synchronously.
      [fetchRequest setValue:cachedUserAgent forHTTPHeaderField:@"User-Agent"];
    } else if (userAgentProvider != nil) {
      // The User-Agent is not cached in memory. Fetch it asynchronously.
      [self updateUserAgentAsynchronouslyForRequest:fetchRequest
                                  userAgentProvider:userAgentProvider
                                       mayAuthorize:mayAuthorize
                                        mayDecorate:mayDecorate];
      // This method can't continue until the User-Agent header is fetched. The above
      // method call will re-enter this method later, but with the User-Agent header set.
      return;
    }
  }

  NSString *effectiveHTTPMethod = [fetchRequest valueForHTTPHeaderField:@"X-HTTP-Method-Override"];
  if (effectiveHTTPMethod == nil) {
    effectiveHTTPMethod = fetchRequest.HTTPMethod;
  }
  BOOL isEffectiveHTTPGet = (effectiveHTTPMethod == nil || [effectiveHTTPMethod isEqual:@"GET"]);

  BOOL needsUploadTask = (self.useUploadTask || self.bodyFileURL || self.bodyStreamProvider);
  if (_bodyData || self.bodyStreamProvider || fetchRequest.HTTPBodyStream) {
    if (isEffectiveHTTPGet) {
      fetchRequest.HTTPMethod = @"POST";
      isEffectiveHTTPGet = NO;
    }

    if (_bodyData) {
      if (!needsUploadTask) {
        fetchRequest.HTTPBody = _bodyData;
      }
#if !STRIP_GTM_FETCH_LOGGING
    } else if (fetchRequest.HTTPBodyStream) {
      if ([self respondsToSelector:@selector(loggedInputStreamForInputStream:)]) {
        fetchRequest.HTTPBodyStream =
            [self performSelector:@selector(loggedInputStreamForInputStream:)
                       withObject:fetchRequest.HTTPBodyStream];
      }
#endif
    }
  }

  // We authorize after setting up the http method and body in the request
  // because OAuth 1 may need to sign the request body
  if (mayAuthorize && _authorizer && !isDataRequest) {
    BOOL isAuthorized = [_authorizer isAuthorizedRequest:fetchRequest];
    if (!isAuthorized) {
      // Authorization needed.
      //
      // If this session is held by the fetcher service, clear the session now so that we don't
      // assume it's still valid after authorization completes.
      if (self.canShareSession) {
        self.session = nil;
      }

      // Authorizing the request will recursively call this beginFetch:mayDelay:
      // or failToBeginFetchWithError:.
      [self authorizeRequest];
      return;
    }
  }

  if (mayDecorate && [_service respondsToSelector:@selector(decorators)]) {
    NSArray<id<GTMFetcherDecoratorProtocol>> *decorators = _service.decorators;
    if (decorators.count) {
      // If this session is held by the fetcher service, clear the session now so that we don't
      // assume it's still valid after decoration completes.
      //
      // The service will still hold on to the session, so as long as decoration doesn't take more
      // than 30 seconds since the last request, the service's session will be re-used when the
      // fetch actually starts.
      if (self.canShareSession) {
        self.session = nil;
      }
      [self applyDecoratorsAtRequestWillStart:decorators startingAtIndex:0];
      return;
    }
  }

  // set the default upload or download retry interval, if necessary
  if ([self isRetryEnabled] && self.maxRetryInterval <= 0) {
    if (isEffectiveHTTPGet || [effectiveHTTPMethod isEqual:@"HEAD"]) {
      [self setMaxRetryInterval:kDefaultMaxDownloadRetryInterval];
    } else {
      [self setMaxRetryInterval:kDefaultMaxUploadRetryInterval];
    }
  }

  // Even though the StartingUp state was entered at the start of the function, we re-enter it here
  // in case a call to `-stopFetcher` comes in on another thread while the above code was running,
  // this lets
  // `-startingState:` do the needed checks/callbacks/cleanups without repeating them here.
  if (![self startingState:kStartingStateStartingUp]) {
    return;
  }

  // finally, start the connection

  NSURLSessionTask *newSessionTask;
  BOOL needsDataAccumulator = NO;
  if (_downloadResumeData) {
    newSessionTask = [_session downloadTaskWithResumeData:_downloadResumeData];
    GTMSESSION_ASSERT_DEBUG_OR_LOG(
        newSessionTask, @"Failed downloadTaskWithResumeData for %@, resume data %lu bytes",
        _session, (unsigned long)_downloadResumeData.length);
  } else if (_destinationFileURL && !isDataRequest) {
    newSessionTask = [_session downloadTaskWithRequest:fetchRequest];
    GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, @"Failed downloadTaskWithRequest for %@, %@",
                                   _session, fetchRequest);
  } else if (needsUploadTask) {
    if (bodyFileURL) {
      newSessionTask = [_session uploadTaskWithRequest:fetchRequest fromFile:bodyFileURL];
      GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask,
                                     @"Failed uploadTaskWithRequest for %@, %@, file %@", _session,
                                     fetchRequest, bodyFileURL.path);
    } else if (self.bodyStreamProvider) {
      newSessionTask = [_session uploadTaskWithStreamedRequest:fetchRequest];
      GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask,
                                     @"Failed uploadTaskWithStreamedRequest for %@, %@", _session,
                                     fetchRequest);
    } else {
      GTMSESSION_ASSERT_DEBUG_OR_LOG(_bodyData != nil, @"Upload task needs body data, %@",
                                     fetchRequest);
      newSessionTask = [_session uploadTaskWithRequest:fetchRequest
                                              fromData:(NSData *_Nonnull)_bodyData];
      GTMSESSION_ASSERT_DEBUG_OR_LOG(
          newSessionTask, @"Failed uploadTaskWithRequest for %@, %@, body data %lu bytes", _session,
          fetchRequest, (unsigned long)_bodyData.length);
    }
    needsDataAccumulator = YES;
  } else {
    newSessionTask = [_session dataTaskWithRequest:fetchRequest];
    needsDataAccumulator = YES;
    GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, @"Failed dataTaskWithRequest for %@, %@",
                                   _session, fetchRequest);
  }
  self.sessionTask = newSessionTask;

  if (!newSessionTask) {
    // We shouldn't get here; if we're here, an earlier assertion should have fired to explain
    // which session task creation failed.
    [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorTaskCreationFailed)];
    return;
  }

  if (needsDataAccumulator && _accumulateDataBlock == nil) {
    self.downloadedData = [NSMutableData data];
  }
  if (_taskDescription) {
    newSessionTask.taskDescription = _taskDescription;
  }
  if (_taskPriority >= 0) {
    newSessionTask.priority = _taskPriority;
  }

#if GTM_DISABLE_FETCHER_TEST_BLOCK
  GTMSESSION_ASSERT_DEBUG(_testBlock == nil && gGlobalTestBlock == nil, @"test blocks disabled");
  _testBlock = nil;
#else
  if (!_testBlock) {
    if (gGlobalTestBlock) {
      // Note that the test block may pass nil for all of its response parameters,
      // indicating that the fetch should actually proceed. This is useful when the
      // global test block has been set, and the app is only testing a specific
      // fetcher.  The block simulation code will then resume the task.
      _testBlock = gGlobalTestBlock;
    }
  }
  _isUsingTestBlock = (_testBlock != nil);
#endif  // GTM_DISABLE_FETCHER_TEST_BLOCK

#if GTM_BACKGROUND_TASK_FETCHING
  id<GTMUIApplicationProtocol> app = [[self class] fetcherUIApplication];
  // Background tasks seem to interfere with out-of-process uploads and downloads.
  if (app && !self.skipBackgroundTask && !self.usingBackgroundSession) {
    // Tell UIApplication that we want to continue even when the app is in the
    // background.
#if DEBUG
    NSString *bgTaskName =
        [NSString stringWithFormat:@"%@-%@", [self class], fetchRequest.URL.host];
#else
    NSString *bgTaskName = @"GTMSessionFetcher";
#endif
    // Since a request can be started from any thread, we also have to ensure the
    // variable for accessing it is safe across the initial thread and the handler
    // (incase it gets failed immediately from the app already heading into the
    // background).
    __block UIBackgroundTaskIdentifier guardedTaskID = UIBackgroundTaskInvalid;
    UIBackgroundTaskIdentifier returnedTaskID =
        [app beginBackgroundTaskWithName:bgTaskName
                       expirationHandler:^{
                         // Background task expiration callback - this block is always invoked by
                         // UIApplication on the main thread.
                         UIBackgroundTaskIdentifier localTaskID;
                         @synchronized(self) {
                           GTMSessionMonitorSynchronized(self);
                           localTaskID = guardedTaskID;
                         }
                         if (localTaskID != UIBackgroundTaskInvalid) {
                           @synchronized(self) {
                             GTMSessionMonitorSynchronized(self);
                             if (localTaskID == self.backgroundTaskIdentifier) {
                               self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
                             }
                           }
                           [app endBackgroundTask:localTaskID];
                         }
                       }];
    @synchronized(self) {
      GTMSessionMonitorSynchronized(self);
      guardedTaskID = returnedTaskID;
      self.backgroundTaskIdentifier = returnedTaskID;
    }
  }
#endif

  if (!_initialRequestDate) {
    _initialRequestDate = [[NSDate alloc] init];
  }

  // We don't expect to reach here even on retry or auth until a stop notification has been sent
  // for the previous task, but we should ensure that we don't unbalance that.
  GTMSESSION_ASSERT_DEBUG(!_isStopNotificationNeeded, @"Start notification without a prior stop");
  [self sendStopNotificationIfNeeded];

  [self addPersistedBackgroundSessionToDefaults];

  [self setStopNotificationNeeded:YES];

  [self postNotificationOnMainThreadWithName:kGTMSessionFetcherStartedNotification userInfo:nil];

  // The service needs to know our task if it is serving as NSURLSession delegate.
  [_service fetcherDidBeginFetching:self];

  if (_testBlock) {
#if !GTM_DISABLE_FETCHER_TEST_BLOCK
    [self simulateFetchForTestBlock];
#endif
  } else {
    // We resume the session task after posting the notification since the
    // delegate callbacks may happen immediately if the fetch is started off
    // the main thread or the session delegate queue is on a background thread,
    // and we don't want to post a start notification after a premature finish
    // of the session task.
    [newSessionTask resume];
  }

  BOOL stopped;
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);
    stopped = _userStoppedFetching;
    if (!stopped) {
      _startingState = kStartingStateNone;
    }
  }
  // If a `-stopFetching` came in between where the connection was finally started (see above), then
  // `-stopFetching` itself didn't do anything because it would be a race, so we have to trigger
  // the cleanup work here manually.
  //
  if (stopped) {
    if (self.stopFetchingTriggersCompletionHandler) {
      NSError *error = [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
                                           code:GTMSessionFetcherErrorUserCancelled
                                       userInfo:nil];
      // Fetch started, can't use `-failToBeginFetchWithError:`.
      [self finishWithError:error shouldRetry:NO];
    } else {
      [self stopFetchReleasingCallbacks:YES];
    }
  }
}

// Helper method to create a new NSURLSession for this fetcher. Because the original
// implementation had this code inline, marking direct to avoid any danger of subclasses
// overriding the behavior.
- (NSURLSession *)createSessionWithDelegate:(id<NSURLSessionDelegate>)sessionDelegate
                          sessionIdentifier:(nullable NSString *)priorSessionIdentifier
    __attribute__((objc_direct)) {
  // Create a session.
  if (!_configuration) {
    if (priorSessionIdentifier || self.usingBackgroundSession) {
      NSString *sessionIdentifier = priorSessionIdentifier;
      if (!sessionIdentifier) {
        sessionIdentifier = [self createSessionIdentifierWithMetadata:nil];
      }
      NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIdentifierToFetcherMap];
      [sessionIdentifierToFetcherMap setObject:self forKey:self.sessionIdentifier];

      _configuration = [NSURLSessionConfiguration
          backgroundSessionConfigurationWithIdentifier:sessionIdentifier];
      self.usingBackgroundSession = YES;
      self.canShareSession = NO;
    } else {
      _configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
    }
#if !GTM_ALLOW_INSECURE_REQUESTS
    _configuration.TLSMinimumSupportedProtocolVersion = tls_protocol_version_TLSv12;
#endif
  }  // !_configuration
  _configuration.HTTPCookieStorage = self.cookieStorage;

  if (_configurationBlock) {
    _configurationBlock(self, _configuration);
  }

  id<NSURLSessionDelegate> delegate = sessionDelegate;
  if (!delegate || !self.canShareSession) {
    delegate = self;
  }
  NSURLSession *session = [NSURLSession sessionWithConfiguration:_configuration
                                                        delegate:delegate
                                                   delegateQueue:self.sessionDelegateQueue];
  GTMSESSION_ASSERT_DEBUG(session, @"Couldn't create session");

  // If this assertion fires, the client probably tried to use a session identifier that was
  // already used. The solution is to make the client use a unique identifier (or better yet let
  // the session fetcher assign the identifier).
  GTMSESSION_ASSERT_DEBUG(session.delegate == delegate, @"Couldn't assign delegate.");

  if (session) {
    BOOL isUsingSharedDelegate = (delegate != self);
    if (!isUsingSharedDelegate) {
      _shouldInvalidateSession = YES;
    }
  }

  return session;
}

// Asynchronously calculates the User-Agent header from |userAgentProvider|, then
// sets it in |fetchRequest| and continues the request.
- (void)updateUserAgentAsynchronouslyForRequest:(NSMutableURLRequest *)fetchRequest
                              userAgentProvider:(id<GTMUserAgentProvider>)userAgentProvider
                                   mayAuthorize:(BOOL)mayAuthorize
                                    mayDecorate:(BOOL)mayDecorate {
  GTMSESSION_LOG_DEBUG_VERBOSE(
      @"GTMSessionFetcher fetching User-Agent from GTMUserAgentProvider %@...", _userAgentProvider);

  if (![self startingState:kStartingStateCalculatingUA]) {
    return;
  }

  __weak __typeof__(self) weakSelf = self;
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    __strong __typeof__(self) strongSelf = weakSelf;
    if (!strongSelf) {
      GTMSESSION_LOG_DEBUG_VERBOSE(@"GTMSessionFetcher deallocated before GTMUserAgentProvider "
                                   @"fetch dispatched, ignoring.");
      return;
    }
    NSString *userAgent = [userAgentProvider userAgent];
    GTMSESSION_LOG_DEBUG_VERBOSE(@"Fetched User-Agent string: [%@]", userAgent);
    GTMSESSION_ASSERT_DEBUG(userAgentProvider.cachedUserAgent != nil,
                            @"GTMUserAgentProvider %@ should have cached user agent now that it's "
                            @"calculated, but returned nil",
                            userAgentProvider);
    @synchronized(strongSelf) {
      GTMSessionMonitorSynchronized(strongSelf);
      // If `stopFetching` was called, do nothing, since the fetch was in a delay state
      // any needed callback already happened.
      if (strongSelf->_userStoppedFetching) {
        return;
      }
      [strongSelf->_request setValue:userAgent forHTTPHeaderField:@"User-Agent"];
      strongSelf->_startingState = kStartingStateNone;
    }
    [strongSelf beginFetchMayDelay:NO mayAuthorize:mayAuthorize mayDecorate:mayDecorate];
  });
}

NSData *_Nullable GTMDataFromInputStream(NSInputStream *inputStream, NSError **outError) {
  NSMutableData *data = [NSMutableData data];

  [inputStream open];
  NSInteger numberOfBytesRead = 0;
  while ([inputStream hasBytesAvailable]) {
    uint8_t buffer[512];
    numberOfBytesRead = [inputStream read:buffer maxLength:sizeof(buffer)];
    if (numberOfBytesRead > 0) {
      [data appendBytes:buffer length:(NSUInteger)numberOfBytesRead];
    } else {
      break;
    }
  }
  [inputStream close];
  NSError *streamError = inputStream.streamError;

  if (streamError) {
    data = nil;
  }
  if (outError) {
    *outError = streamError;
  }
  return data;
}

#if !GTM_DISABLE_FETCHER_TEST_BLOCK

- (void)simulateFetchForTestBlock {
  // This is invoked on the same thread as the beginFetch method was.
  //
  // Callbacks will all occur on the callback queue.
  _testBlock(self, ^(NSURLResponse *response, NSData *responseData, NSError *error) {
    // Callback from test block.
    if (response == nil && responseData == nil && error == nil) {
      // Assume the fetcher should execute rather than be tested.
      self->_testBlock = nil;
      self->_isUsingTestBlock = NO;
      [self->_sessionTask resume];
      return;
    }

    GTMSessionFetcherBodyStreamProvider bodyStreamProvider = self.bodyStreamProvider;
    if (bodyStreamProvider) {
      bodyStreamProvider(^(NSInputStream *bodyStream) {
        // Read from the input stream into an NSData buffer.  We'll drain the stream
        // explicitly on a background queue.
        [self
            invokeOnCallbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
                 afterUserStopped:NO
                            block:^{
                              NSError *streamError;
                              NSData *streamedData =
                                  GTMDataFromInputStream(bodyStream, &streamError);

                              dispatch_async(dispatch_get_main_queue(), ^{
                                // Continue callbacks on the main thread, since serial behavior
                                // is more reliable for tests.
                                [self
                                    simulateDataCallbacksForTestBlockWithBodyData:streamedData
                                                                         response:response
                                                                     responseData:responseData
                                                                            error:
                                                                                (error
                                                                                     ?: streamError)];
                              });
                            }];
      });
    } else {
      // No input stream; use the supplied data or file URL.
      NSURL *bodyFileURL = self.bodyFileURL;
      if (bodyFileURL) {
        NSError *readError;
        self->_bodyData = [NSData dataWithContentsOfURL:bodyFileURL
                                                options:NSDataReadingMappedIfSafe
                                                  error:&readError];
        error = readError;
      }

      // No stream provider.

      // In real fetches, nothing happens until the run loop spins, so apps have leeway to
      // set callbacks after they call beginFetch. We'll mirror that fetcher behavior by
      // delaying callbacks here at least to the next spin of the run loop.  That keeps
      // immediate, synchronous setting of callback blocks after beginFetch working in tests.
      dispatch_async(dispatch_get_main_queue(), ^{
        [self simulateDataCallbacksForTestBlockWithBodyData:self->_bodyData
                                                   response:response
                                               responseData:responseData
                                                      error:error];
      });
    }
  });
}

- (void)simulateByteTransferReportWithDataLength:(int64_t)totalDataLength
                                           block:(GTMSessionFetcherSendProgressBlock)block {
  // This utility method simulates transfer progress with up to three callbacks.
  // It is used to call back to any of the progress blocks.
  int64_t sendReportSize = totalDataLength / 3 + 1;
  int64_t totalSent = 0;
  while (totalSent < totalDataLength) {
    int64_t bytesRemaining = totalDataLength - totalSent;
    sendReportSize = MIN(sendReportSize, bytesRemaining);
    totalSent += sendReportSize;
    [self invokeOnCallbackQueueUnlessStopped:^{
      block(sendReportSize, totalSent, totalDataLength);
    }];
  }
}

- (void)simulateDataCallbacksForTestBlockWithBodyData:(nullable NSData *)bodyData
                                             response:(NSURLResponse *)response
                                         responseData:(NSData *)suppliedData
                                                error:(NSError *)suppliedError {
  __block NSData *responseData = suppliedData;
  __block NSError *responseError = suppliedError;

  // This method does the test simulation of callbacks once the upload
  // and download data are known.
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    // Get copies of ivars we'll access in async invocations.  This simulation assumes
    // they won't change during fetcher execution.
    NSURL *destinationFileURL = _destinationFileURL;
    GTMSessionFetcherWillRedirectBlock willRedirectBlock = _willRedirectBlock;
    GTMSessionFetcherDidReceiveResponseBlock didReceiveResponseBlock = _didReceiveResponseBlock;
    GTMSessionFetcherSendProgressBlock sendProgressBlock = _sendProgressBlock;
    GTMSessionFetcherDownloadProgressBlock downloadProgressBlock = _downloadProgressBlock;
    GTMSessionFetcherAccumulateDataBlock accumulateDataBlock = _accumulateDataBlock;
    GTMSessionFetcherReceivedProgressBlock receivedProgressBlock = _receivedProgressBlock;
    GTMSessionFetcherWillCacheURLResponseBlock willCacheURLResponseBlock =
        _willCacheURLResponseBlock;
    GTMSessionFetcherChallengeBlock challengeBlock = _challengeBlock;

    // Simulate receipt of redirection.
    if (willRedirectBlock) {
      __auto_type block = ^{
        willRedirectBlock((NSHTTPURLResponse *)response, self->_request,
                          ^(NSURLRequest *redirectRequest){
                              // For simulation, we'll assume
                              // the app will just continue.
                          });
      };
      [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES block:block];
    }

    // If the fetcher has a challenge block, simulate a challenge.
    //
    // It might be nice to eventually let the user determine which testBlock
    // fetches get challenged rather than always executing the supplied
    // challenge block.
    if (challengeBlock) {
      __auto_type block = ^{
        NSURL *requestURL = self->_request.URL;
        NSString *host = requestURL.host;
        NSURLProtectionSpace *pspace =
            [[NSURLProtectionSpace alloc] initWithHost:host
                                                  port:requestURL.port.integerValue
                                              protocol:requestURL.scheme
                                                 realm:nil
                                  authenticationMethod:NSURLAuthenticationMethodHTTPBasic];
        id<NSURLAuthenticationChallengeSender> unusedSender =
            (id<NSURLAuthenticationChallengeSender>)[NSNull null];
        NSURLAuthenticationChallenge *challenge =
            [[NSURLAuthenticationChallenge alloc] initWithProtectionSpace:pspace
                                                       proposedCredential:nil
                                                     previousFailureCount:0
                                                          failureResponse:nil
                                                                    error:nil
                                                                   sender:unusedSender];
        challengeBlock(self, challenge,
                       ^(NSURLSessionAuthChallengeDisposition disposition,
                         NSURLCredential *_Nullable credential){
                           // We could change the
                           // responseData and responseError
                           // based on the disposition,
                           // but it's easier for apps to
                           // just supply the expected data
                           // and error
                           // directly to the test block. So
                           // this simulation ignores the
                           // disposition.
                       });
      };
      [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES block:block];
    }

    // Simulate receipt of an initial response.
    if (response && didReceiveResponseBlock) {
      __auto_type block = ^{
        didReceiveResponseBlock(response, ^(NSURLSessionResponseDisposition desiredDisposition){
                                    // For simulation, we'll assume
                                    // the disposition is to continue.
                                });
      };
      [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES block:block];
    }

    // Simulate reporting send progress.
    if (sendProgressBlock) {
      __auto_type block =
          ^(int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend) {
            // This is invoked on the callback queue unless
            // stopped.
            sendProgressBlock(bytesSent, totalBytesSent, totalBytesExpectedToSend);
          };
      [self simulateByteTransferReportWithDataLength:(int64_t)bodyData.length block:block];
    }

    if (destinationFileURL) {
      // Simulate download to file progress.
      if (downloadProgressBlock) {
        __auto_type block = ^(int64_t bytesDownloaded, int64_t totalBytesDownloaded,
                              int64_t totalBytesExpectedToDownload) {
          // This is invoked on the callback queue unless
          // stopped.
          downloadProgressBlock(bytesDownloaded, totalBytesDownloaded,
                                totalBytesExpectedToDownload);
        };
        [self simulateByteTransferReportWithDataLength:(int64_t)responseData.length block:block];
      }

      NSError *writeError;
      [responseData writeToURL:destinationFileURL options:NSDataWritingAtomic error:&writeError];
      if (writeError) {
        // Tell the test code that writing failed.
        responseError = writeError;
      }
    } else {
      // Simulate download to NSData progress.
      if ((accumulateDataBlock || receivedProgressBlock) && responseData) {
        __auto_type block = ^(NSData *data, int64_t bytesReceived, int64_t totalBytesReceived,
                              int64_t totalBytesExpectedToReceive) {
          // This is invoked on the callback queue unless stopped.
          if (accumulateDataBlock) {
            accumulateDataBlock(data);
          }

          if (receivedProgressBlock) {
            receivedProgressBlock(bytesReceived, totalBytesReceived);
          }
        };
        [self simulateByteTransferWithData:responseData block:block];
      }

      if (!accumulateDataBlock) {
        _downloadedData = [responseData mutableCopy];
      }

      if (willCacheURLResponseBlock) {
        // Simulate letting the client inspect and alter the cached response.
        NSData *cachedData = responseData ?: [[NSData alloc] init];  // Always have non-nil data.
        NSCachedURLResponse *cachedResponse =
            [[NSCachedURLResponse alloc] initWithResponse:response data:cachedData];
        __auto_type block = ^{
          willCacheURLResponseBlock(cachedResponse, ^(NSCachedURLResponse *responseToCache){
                                        // The app may provide an
                                        // alternative response, or
                                        // nil to defeat caching.
                                    });
        };
        [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES block:block];
      }
    }
    _response = response;
  }  // @synchronized(self)

  NSOperationQueue *queue = self.sessionDelegateQueue;
  [queue addOperationWithBlock:^{
    // Rather than invoke failToBeginFetchWithError: we want to simulate completion of
    // a connection that started and ended, so we'll call down to finishWithError:
    NSInteger status = responseError ? responseError.code : 200;
    if (status >= 200 && status <= 399) {
      [self finishWithError:nil shouldRetry:NO];
    } else {
      [self shouldRetryNowForStatus:status
                              error:responseError
                   forceAssumeRetry:NO
                           response:^(BOOL shouldRetry) {
                             [self finishWithError:responseError shouldRetry:shouldRetry];
                           }];
    }
  }];
}

- (void)simulateByteTransferWithData:(NSData *)responseData
                               block:(GTMSessionFetcherSimulateByteTransferBlock)transferBlock {
  // This utility method simulates transfering data to the client. It divides the data into at most
  // "chunkCount" chunks and then passes each chunk along with a progress update to transferBlock.
  // This function can be used with accumulateDataBlock or receivedProgressBlock.

  NSUInteger chunkCount = MAX(self.testBlockAccumulateDataChunkCount, (NSUInteger)1);
  NSUInteger totalDataLength = responseData.length;
  NSUInteger sendDataSize = totalDataLength / chunkCount + 1;
  NSUInteger totalSent = 0;
  while (totalSent < totalDataLength) {
    NSUInteger bytesRemaining = totalDataLength - totalSent;
    sendDataSize = MIN(sendDataSize, bytesRemaining);
    NSData *chunkData = [responseData subdataWithRange:NSMakeRange(totalSent, sendDataSize)];
    totalSent += sendDataSize;
    [self invokeOnCallbackQueueUnlessStopped:^{
      transferBlock(chunkData, (int64_t)sendDataSize, (int64_t)totalSent, (int64_t)totalDataLength);
    }];
  }
}

#endif  // !GTM_DISABLE_FETCHER_TEST_BLOCK

- (void)setSessionTask:(NSURLSessionTask *)sessionTask {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (_sessionTask != sessionTask) {
      _sessionTask = sessionTask;
      if (_sessionTask) {
        // Request could be nil on restoring this fetcher from a background session.
        if (!_request) {
          _request = [_sessionTask.originalRequest mutableCopy];
        }
      }
    }
  }  // @synchronized(self)
}

- (nullable NSURLSessionTask *)sessionTask {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _sessionTask;
  }  // @synchronized(self)
}

+ (NSUserDefaults *)fetcherUserDefaults {
  static NSUserDefaults *gFetcherUserDefaults = nil;

  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    Class fetcherUserDefaultsClass = NSClassFromString(@"GTMSessionFetcherUserDefaultsFactory");
    if (fetcherUserDefaultsClass) {
      gFetcherUserDefaults = [fetcherUserDefaultsClass fetcherUserDefaults];
    } else {
      gFetcherUserDefaults = [NSUserDefaults standardUserDefaults];
    }
  });
  return gFetcherUserDefaults;
}

- (void)addPersistedBackgroundSessionToDefaults {
  NSString *sessionIdentifier = self.sessionIdentifier;
  if (!sessionIdentifier) {
    return;
  }
  NSArray *oldBackgroundSessions = [[self class] activePersistedBackgroundSessions];
  if ([oldBackgroundSessions containsObject:_sessionIdentifier]) {
    return;
  }
  NSMutableArray *newBackgroundSessions = [NSMutableArray arrayWithArray:oldBackgroundSessions];
  [newBackgroundSessions addObject:sessionIdentifier];
  GTM_LOG_BACKGROUND_SESSION(@"Add to background sessions: %@", newBackgroundSessions);

  NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults];
  [userDefaults setObject:newBackgroundSessions forKey:kGTMSessionFetcherPersistedDestinationKey];
  [userDefaults synchronize];
}

- (void)removePersistedBackgroundSessionFromDefaults {
  NSString *sessionIdentifier = self.sessionIdentifier;
  if (!sessionIdentifier) return;

  NSArray *oldBackgroundSessions = [[self class] activePersistedBackgroundSessions];
  if (!oldBackgroundSessions) {
    return;
  }
  NSMutableArray *newBackgroundSessions = [NSMutableArray arrayWithArray:oldBackgroundSessions];
  NSUInteger sessionIndex = [newBackgroundSessions indexOfObject:sessionIdentifier];
  if (sessionIndex == NSNotFound) {
    return;
  }
  [newBackgroundSessions removeObjectAtIndex:sessionIndex];
  GTM_LOG_BACKGROUND_SESSION(@"Remove from background sessions: %@", newBackgroundSessions);

  NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults];
  if (newBackgroundSessions.count == 0) {
    [userDefaults removeObjectForKey:kGTMSessionFetcherPersistedDestinationKey];
  } else {
    [userDefaults setObject:newBackgroundSessions forKey:kGTMSessionFetcherPersistedDestinationKey];
  }
  [userDefaults synchronize];
}

+ (nullable NSArray *)activePersistedBackgroundSessions {
  NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults];
  NSArray *oldBackgroundSessions =
      [userDefaults arrayForKey:kGTMSessionFetcherPersistedDestinationKey];
  if (oldBackgroundSessions.count == 0) {
    return nil;
  }
  NSMutableArray *activeBackgroundSessions = nil;
  NSMapTable *sessionIdentifierToFetcherMap = [self sessionIdentifierToFetcherMap];
  for (NSString *sessionIdentifier in oldBackgroundSessions) {
    GTMSessionFetcher *fetcher = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier];
    if (fetcher) {
      if (!activeBackgroundSessions) {
        activeBackgroundSessions = [[NSMutableArray alloc] init];
      }
      [activeBackgroundSessions addObject:sessionIdentifier];
    }
  }
  return activeBackgroundSessions;
}

+ (NSArray *)fetchersForBackgroundSessions {
  NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults];
  NSArray *backgroundSessions =
      [userDefaults arrayForKey:kGTMSessionFetcherPersistedDestinationKey];
  NSMapTable *sessionIdentifierToFetcherMap = [self sessionIdentifierToFetcherMap];
  NSMutableArray *fetchers = [NSMutableArray array];
  for (NSString *sessionIdentifier in backgroundSessions) {
    GTMSessionFetcher *fetcher = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier];
    if (!fetcher) {
      fetcher = [self fetcherWithSessionIdentifier:sessionIdentifier];
      GTMSESSION_ASSERT_DEBUG(fetcher != nil, @"Unexpected invalid session identifier: %@",
                              sessionIdentifier);
      if (!fetcher.clientWillReconnectBackgroundSession) {
        [fetcher beginFetchWithCompletionHandler:nil];
      }
    }
    GTM_LOG_BACKGROUND_SESSION(@"%@ restoring session %@ by creating fetcher %@ %p", [self class],
                               sessionIdentifier, fetcher, fetcher);
    if (fetcher != nil) {
      [fetchers addObject:fetcher];
    }
  }
  return fetchers;
}

#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
+ (void)application:(UIApplication *)application
    handleEventsForBackgroundURLSession:(NSString *)identifier
                      completionHandler:(GTMSessionFetcherSystemCompletionHandler)completionHandler
    GTM_SWIFT_DISABLE_ASYNC {
  GTMSessionFetcher *fetcher = [self fetcherWithSessionIdentifier:identifier];
  if (fetcher != nil) {
    fetcher.systemCompletionHandler = completionHandler;
  } else {
    GTM_LOG_BACKGROUND_SESSION(@"%@ did not create background session identifier: %@", [self class],
                               identifier);
  }
}
#endif

- (nullable NSString *)sessionIdentifier {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _sessionIdentifier;
  }  // @synchronized(self)
}

- (void)setSessionIdentifier:(NSString *)sessionIdentifier {
  GTMSESSION_ASSERT_DEBUG(sessionIdentifier != nil, @"Invalid session identifier");
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    GTMSESSION_ASSERT_DEBUG(!_session, @"Unable to set session identifier after session created");
    _sessionIdentifier = [sessionIdentifier copy];
    _usingBackgroundSession = YES;
    _canShareSession = NO;
    [self restoreDefaultStateForSessionIdentifierMetadata];
  }  // @synchronized(self)
}

- (void)setSessionIdentifierInternal:(nullable NSString *)sessionIdentifier {
  // This internal method only does a synchronized set of the session identifier.
  // It does not have side effects on the background session, shared session, or
  // session identifier metadata.
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _sessionIdentifier = [sessionIdentifier copy];
  }  // @synchronized(self)
}

- (nullable NSDictionary<NSString *, NSString *> *)sessionUserInfo {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (_sessionUserInfo == nil) {
      // We'll return the metadata dictionary with internal keys removed. This avoids the user
      // re-using the userInfo dictionary later and accidentally including the internal keys.
      // Just incase something got corrupted in storage and parsed back out differently, ensure
      // the api contract on types is still valid.
      NSMutableDictionary *metadata = [[self sessionIdentifierMetadataUnsynchronized] mutableCopy];
      NSSet *keysToRemove = [metadata keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) {
        return ![key isKindOfClass:[NSString class]] || ![obj isKindOfClass:[NSString class]] ||
               [key hasPrefix:@"_"];
      }];
#if DEBUG
      // If we're pruning, give warnings about the things that were invalid as some bug has slipped
      // through.
      if (keysToRemove.count) {
        [metadata enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj,
                                                      BOOL *_Nonnull stop) {
          if (![key isKindOfClass:[NSString class]]) {
            GTMSESSION_LOG_DEBUG(
                @"InternalError: restoring sessionUserInfo is pruning a non String key: %@", key);
          } else if (![obj isKindOfClass:[NSString class]]) {
            GTMSESSION_LOG_DEBUG(
                @"InternalError: restoring sessionUserInfo is pruning a non String value: %@: %@",
                key, obj);
          }
        }];
      }
#endif  // DEBUG
      [metadata removeObjectsForKeys:[keysToRemove allObjects]];

      if (metadata.count > 0) {
        _sessionUserInfo = metadata;
      }
    }
    return _sessionUserInfo;
  }  // @synchronized(self)
}

- (void)setSessionUserInfo:(nullable NSDictionary<NSString *, NSString *> *)dictionary {
  [dictionary enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj,
                                                  BOOL *_Nonnull stop) {
    if (![key isKindOfClass:[NSString class]]) {
      [NSException raise:NSInvalidArgumentException
                  format:@"sessionUserInfo keys must be NSStrings: %@", key];
    }
    if ([key hasPrefix:@"_"]) {
      [NSException
           raise:NSInvalidArgumentException
          format:
              @"sessionUserInfo keys starting with an underscore are reserved for the library: %@",
              key];
    }
    if (![obj isKindOfClass:[NSString class]]) {
      [NSException raise:NSInvalidArgumentException
                  format:@"Values in sessionUserInfo must be NSStrings: %@: %@", key, obj];
    }
  }];

  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    GTMSESSION_ASSERT_DEBUG(_sessionIdentifier == nil, @"Too late to assign userInfo");
    _sessionUserInfo = dictionary;
  }  // @synchronized(self)
}

- (nullable NSDictionary *)sessionIdentifierDefaultMetadata {
  GTMSessionCheckSynchronized(self);

  NSMutableDictionary *defaultUserInfo = [[NSMutableDictionary alloc] init];
  if (_destinationFileURL) {
    defaultUserInfo[kGTMSessionIdentifierDestinationFileURLMetadataKey] =
        [_destinationFileURL absoluteString];
  }
  if (_bodyFileURL) {
    defaultUserInfo[kGTMSessionIdentifierBodyFileURLMetadataKey] = [_bodyFileURL absoluteString];
  }
  if (_clientWillReconnectBackgroundSession) {
    defaultUserInfo[kGTMSessionIdentifierClientReconnectMetadataKey] = @"YES";
  }
  return (defaultUserInfo.count > 0) ? defaultUserInfo : nil;
}

- (void)restoreDefaultStateForSessionIdentifierMetadata {
  GTMSessionCheckSynchronized(self);

  NSDictionary *metadata = [self sessionIdentifierMetadataUnsynchronized];
  NSString *destinationFileURLString = metadata[kGTMSessionIdentifierDestinationFileURLMetadataKey];
  if (destinationFileURLString) {
    _destinationFileURL = [NSURL URLWithString:destinationFileURLString];
    GTM_LOG_BACKGROUND_SESSION(@"Restoring destination file URL: %@", _destinationFileURL);
  }
  NSString *bodyFileURLString = metadata[kGTMSessionIdentifierBodyFileURLMetadataKey];
  if (bodyFileURLString) {
    _bodyFileURL = [NSURL URLWithString:bodyFileURLString];
    GTM_LOG_BACKGROUND_SESSION(@"Restoring body file URL: %@", _bodyFileURL);
  }
  NSString *clientReconnectString = metadata[kGTMSessionIdentifierClientReconnectMetadataKey];
  if (clientReconnectString) {
    _clientWillReconnectBackgroundSession = [clientReconnectString boolValue];
    GTM_LOG_BACKGROUND_SESSION(@"Restoring clientWillReconnectBackgroundSession: %@",
                               (_clientWillReconnectBackgroundSession ? @"YES" : @"NO"));
  }
}

- (nullable NSDictionary<NSString *, id> *)sessionIdentifierMetadata {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return [self sessionIdentifierMetadataUnsynchronized];
  }
}

- (nullable NSDictionary<NSString *, id> *)sessionIdentifierMetadataUnsynchronized {
  GTMSessionCheckSynchronized(self);

  // Session Identifier format: "com.google.<ClassName>_<UUID>_<Metadata in JSON format>
  if (!_sessionIdentifier) {
    return nil;
  }
  NSScanner *metadataScanner = [NSScanner scannerWithString:_sessionIdentifier];
  [metadataScanner setCharactersToBeSkipped:nil];
  NSString *metadataString;
  NSString *uuid;
  if ([metadataScanner scanUpToString:@"_" intoString:NULL] &&
      [metadataScanner scanString:@"_" intoString:NULL] &&
      [metadataScanner scanUpToString:@"_" intoString:&uuid] &&
      [metadataScanner scanString:@"_" intoString:NULL] &&
      [metadataScanner scanUpToString:@"\n" intoString:&metadataString]) {
    _sessionIdentifierUUID = uuid;
    NSData *metadataData = [metadataString dataUsingEncoding:NSUTF8StringEncoding];
    NSError *error;
    NSDictionary *metadataDict = [NSJSONSerialization JSONObjectWithData:metadataData
                                                                 options:0
                                                                   error:&error];
    GTM_LOG_BACKGROUND_SESSION(@"User Info from session identifier: %@ %@", metadataDict,
                               error ? error : @"");
    return metadataDict;
  }
  return nil;
}

- (NSString *)createSessionIdentifierWithMetadata:(nullable NSDictionary *)metadataToInclude {
  NSString *result;
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    // Session Identifier format: "com.google.<ClassName>_<UUID>_<Metadata in JSON format>
    GTMSESSION_ASSERT_DEBUG(!_sessionIdentifier, @"Session identifier already created");
    _sessionIdentifierUUID = [[NSUUID UUID] UUIDString];
    _sessionIdentifier =
        [NSString stringWithFormat:@"%@_%@", kGTMSessionIdentifierPrefix, _sessionIdentifierUUID];

    // Start with user-supplied keys so they cannot accidentally override the fetcher's keys.
    NSMutableDictionary *metadataDict =
        [NSMutableDictionary dictionaryWithDictionary:(NSDictionary *_Nonnull)_sessionUserInfo];

    // sessionUserInfo was declared as `strong` (not `copy`), so it could have been modifed after
    // having been set. So remove anything that breaks the contract.
    NSSet *keysToRemove = [metadataDict keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) {
      return ![key isKindOfClass:[NSString class]] || ![obj isKindOfClass:[NSString class]] ||
             [key hasPrefix:@"_"];
    }];
#if DEBUG
    // If we're pruning, give warnings about the things that were invalid as some bug has slipped
    // through.
    if (keysToRemove.count) {
      [metadataDict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj,
                                                        BOOL *_Nonnull stop) {
        if (![key isKindOfClass:[NSString class]]) {
          GTMSESSION_LOG_DEBUG(
              @"Warning: sessionUserInfo has been modifed to include a non String key: %@", key);
        } else if (![obj isKindOfClass:[NSString class]]) {
          GTMSESSION_LOG_DEBUG(
              @"Warning: sessionUserInfo has been modifed to include a non String value: %@: %@",
              key, obj);
        }
      }];
    }
#endif  // DEBUG
    [metadataDict removeObjectsForKeys:[keysToRemove allObjects]];

    if (metadataToInclude) {
#if DEBUG
      [metadataToInclude enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj,
                                                             BOOL *_Nonnull stop) {
        GTMSESSION_ASSERT_DEBUG([key isKindOfClass:[NSString class]],
                                @"metadataToInclude keys must be NSStrings: %@", key);
        GTMSESSION_ASSERT_DEBUG([key hasPrefix:@"_"],
                                @"metadataToInclude should only have prefixed keys: %@ - %@", key,
                                obj);
      }];
#endif
      [metadataDict addEntriesFromDictionary:(NSDictionary *)metadataToInclude];
    }
    NSDictionary *defaultMetadataDict = [self sessionIdentifierDefaultMetadata];
    if (defaultMetadataDict) {
#if DEBUG
      [defaultMetadataDict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj,
                                                               BOOL *_Nonnull stop) {
        GTMSESSION_ASSERT_DEBUG([key isKindOfClass:[NSString class]],
                                @"defaultMetadataDict keys must be NSStrings: %@", key);
        GTMSESSION_ASSERT_DEBUG([key hasPrefix:@"_"],
                                @"defaultMetadataDict should only have prefixed keys: %@ - %@", key,
                                obj);
      }];
#endif
      [metadataDict addEntriesFromDictionary:defaultMetadataDict];
    }
    if (metadataDict.count > 0) {
      NSData *metadataData = [NSJSONSerialization dataWithJSONObject:metadataDict
                                                             options:0
                                                               error:NULL];
      GTMSESSION_ASSERT_DEBUG(metadataData != nil,
                              @"Session identifier user info failed to convert to JSON");
      if (metadataData.length > 0) {
        NSString *metadataString = [[NSString alloc] initWithData:metadataData
                                                         encoding:NSUTF8StringEncoding];
        _sessionIdentifier = [_sessionIdentifier stringByAppendingFormat:@"_%@", metadataString];
      }
    }
    _didCreateSessionIdentifier = YES;
    result = _sessionIdentifier;
  }  // @synchronized(self)
  return result;
}

- (void)failToBeginFetchWithError:(NSError *)error {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _hasStoppedFetching = YES;
  }

  if (error == nil) {
    error = [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
                                code:GTMSessionFetcherErrorDownloadFailed
                            userInfo:nil];
  }

  [self invokeFetchCallbacksOnCallbackQueueWithData:nil
                                              error:error
                                        mayDecorate:YES
                             shouldReleaseCallbacks:YES];

  [_service fetcherDidStop:self];

  self.authorizer = nil;
}

+ (GTMSessionCookieStorage *)staticCookieStorage {
  static GTMSessionCookieStorage *gCookieStorage = nil;

  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    gCookieStorage = [[GTMSessionCookieStorage alloc] init];
  });
  return gCookieStorage;
}

#if GTM_BACKGROUND_TASK_FETCHING

- (void)endBackgroundTask {
  // Whenever the connection stops or background execution expires,
  // we need to tell UIApplication we're done.
  UIBackgroundTaskIdentifier bgTaskID;
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    bgTaskID = self.backgroundTaskIdentifier;
    if (bgTaskID != UIBackgroundTaskInvalid) {
      self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
    }
  }

  if (bgTaskID != UIBackgroundTaskInvalid) {
    id<GTMUIApplicationProtocol> app = [[self class] fetcherUIApplication];
    [app endBackgroundTask:bgTaskID];
  }
}

#endif  // GTM_BACKGROUND_TASK_FETCHING

- (void)authorizeRequest {
  GTMSessionCheckNotSynchronized(self);

  if (![self startingState:kStartingStateAuthorizing]) {
    return;
  }

  id authorizer = self.authorizer;
  // Prefer the block-based implementation. This *is* a change in behavior, but if authorizers
  // previously provided this method they would presumably assume they can be used for the same
  // requests as before.
  if ([authorizer respondsToSelector:@selector(authorizeRequest:completionHandler:)]) {
    // It's unknown how long an authorizer maintains ownership of the provided block, so
    // avoid potential retain cycles on self and the authorizer.
    __weak __typeof__(self) weakSelf = self;
    NSMutableURLRequest *mutableRequest = [self.request mutableCopy];
    [authorizer authorizeRequest:mutableRequest
               completionHandler:^(NSError *_Nullable error) {
                 [weakSelf authorizer:nil request:mutableRequest finishedWithError:error];
               }];
  } else if ([authorizer respondsToSelector:@selector(authorizeRequest:
                                                              delegate:didFinishSelector:)]) {
    SEL callbackSel = @selector(authorizer:request:finishedWithError:);
    NSMutableURLRequest *mutableRequest = [self.request mutableCopy];
    [authorizer authorizeRequest:mutableRequest delegate:self didFinishSelector:callbackSel];
  } else {
    GTMSESSION_ASSERT_DEBUG(authorizer == nil, @"invalid authorizer for fetch");
    // Should really never get here the main flow shouldn't have called here if there
    // wasn't an authorizer, but for safety sake, continue on through the starting process.
    @synchronized(self) {
      GTMSessionMonitorSynchronized(self);
      _startingState = kStartingStateNone;
    }
    [self beginFetchMayDelay:NO mayAuthorize:NO mayDecorate:YES];
  }
}

// The authorizer parameter is unused, and the block-based callback will never pass
// non-nil; the field is only for the deprecated selector-based implementation for
// legacy reasons.
- (void)authorizer:(nullable id __unused)auth
              request:(nullable NSMutableURLRequest *)authorizedRequest
    finishedWithError:(nullable NSError *)error {
  GTMSessionCheckNotSynchronized(self);

  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    // If `stopFetching` was called, do nothing, since the fetch was in a delay state
    // any needed callback already happened.
    if (_userStoppedFetching) {
      return;
    }
    if (error == nil) {
      _request = authorizedRequest;
      // Clear the delay state only if things aren't about to fail. Don't want to have a race
      // between clearing the state and when the error is posted because some other thread happened
      // to get in a call to
      // `-stopFetching`.
      _startingState = kStartingStateNone;
    }
  }  // @synchronized(self)

  if (error != nil) {
    // We can't fetch without authorization
    [self failToBeginFetchWithError:(NSError *_Nonnull)error];
  } else {
    [self beginFetchMayDelay:NO mayAuthorize:NO mayDecorate:YES];
  }
}

- (void)applyDecoratorsAtRequestWillStart:(NSArray<id<GTMFetcherDecoratorProtocol>> *)decorators
                          startingAtIndex:(NSUInteger)index {
  GTMSessionCheckNotSynchronized(self);
  if (index >= decorators.count) {
    GTMSESSION_LOG_DEBUG_VERBOSE(
        @"GTMSessionFetcher decorate requestWillStart %zu decorators complete", decorators.count);
    @synchronized(self) {
      GTMSessionMonitorSynchronized(self);
      _startingState = kStartingStateNone;
    }
    [self beginFetchMayDelay:NO mayAuthorize:NO mayDecorate:NO];
    return;
  }

  if (index == 0) {
    if (![self startingState:kStartingStateApplyingDecorators]) {
      return;
    }
  }

  __weak __typeof__(self) weakSelf = self;
  id<GTMFetcherDecoratorProtocol> decorator = decorators[index];
  GTMSESSION_LOG_DEBUG_VERBOSE(
      @"GTMSessionFetcher decorate requestWillStart %zu decorators, index %zu, "
      @"retry count %zu, decorator %@",
      decorators.count, index, self.retryCount, decorator);
  [decorator
       fetcherWillStart:self
      completionHandler:^(NSURLRequest *_Nullable newRequest, NSError *_Nullable error) {
        GTMSESSION_LOG_DEBUG_VERBOSE(@"GTMSessionFetcher decorator requestWillStart index %zu "
                                     @"complete, newRequest %@, error %@",
                                     index, newRequest, error);
        __strong __typeof__(self) strongSelf = weakSelf;
        if (!strongSelf) {
          GTMSESSION_LOG_DEBUG(@"GTMSessionFetcher destroyed before requestWillStart "
                               @"decorators completed, ignoring.");
          return;
        }

        BOOL shouldStop;
        @synchronized(strongSelf) {
          GTMSessionMonitorSynchronized(strongSelf);

          shouldStop = strongSelf->_userStoppedFetching;
          if (shouldStop) {
            if (strongSelf->_stopFetchingTriggersCompletionHandler) {
              // Override any error with the cancel error and then trigger the callbacks below.
              //
              // The callback for a cancel is sent from here instead of from within `-stopFetching`
              // like is done for all the other pending states because if `-stopFetching` directly
              // triggered the callbacks, the decorators would also get triggered and in a
              // multithreaded case it would be possible for this current decorartor to have
              // didFinish invoked at the same time but on a different thread; so triggering the
              // failure completion here will avoid that.
              error = [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
                                          code:GTMSessionFetcherErrorUserCancelled
                                      userInfo:nil];
            } else {
              // Suppress any error from being sent due to the `-stopFetching`.
              error = nil;
            }
          }
        }

        if (error) {
          [strongSelf failToBeginFetchWithError:(NSError *_Nonnull)error];
          return;
        }
        if (shouldStop) {
          return;
        }
        if (newRequest) {
          // Copying `NSURLRequest` should be cheap, but in case profiling shows this
          // operation is prohibitively expensive, this API might need to be changed to allow
          // clients to manipulate `self.request` directly.
          [strongSelf updateMutableRequest:[newRequest mutableCopy]];
        }
        [strongSelf applyDecoratorsAtRequestWillStart:decorators startingAtIndex:index + 1];
      }];
}

- (void)applyDecoratorsAtRequestDidFinish:(NSArray<id<GTMFetcherDecoratorProtocol>> *)decorators
                                 withData:(nullable NSData *)data
                                    error:(nullable NSError *)error
                          startingAtIndex:(NSUInteger)index
                   shouldReleaseCallbacks:(BOOL)shouldReleaseCallbacks {
  GTMSessionCheckNotSynchronized(self);

  // NOTE: At this point, the fetch is done, so if `-stopFetching` comes in now, it does not stop
  // these from getting called as the completion is currently being invoked with the results of
  // the fetch.

  if (index >= decorators.count) {
    GTMSESSION_LOG_DEBUG_VERBOSE(
        @"GTMSessionFetcher decorate requestDidFinish %zu decorators complete", decorators.count);
    [self invokeFetchCallbacksOnCallbackQueueWithData:data
                                                error:error
                                          mayDecorate:NO
                               shouldReleaseCallbacks:shouldReleaseCallbacks];
    return;
  }

  __weak __typeof__(self) weakSelf = self;
  id<GTMFetcherDecoratorProtocol> decorator = decorators[index];
  GTMSESSION_LOG_DEBUG_VERBOSE(
      @"GTMSessionFetcher decorate requestDidFinish %zu decorators, index %zu, "
      @"retry count %zu, decorator %@",
      decorators.count, index, self.retryCount, decorator);
  [decorator fetcherDidFinish:self
                     withData:data
                        error:error
            completionHandler:^{
              GTMSESSION_LOG_DEBUG_VERBOSE(
                  @"GTMSessionFetcher decorator requestDidFinish index %zu complete", index);
              __strong __typeof__(self) strongSelf = weakSelf;
              if (!strongSelf) {
                GTMSESSION_LOG_DEBUG(@"GTMSessionFetcher destroyed before requestDidFinish "
                                     @"decorators completed, ignoring.");
                return;
              }
              [strongSelf applyDecoratorsAtRequestDidFinish:decorators
                                                   withData:data
                                                      error:error
                                            startingAtIndex:index + 1
                                     shouldReleaseCallbacks:shouldReleaseCallbacks];
            }];
}

- (BOOL)canFetchWithBackgroundSession {
  // Subclasses may override.
  return YES;
}

// Returns YES if the fetcher has been started and has not yet stopped.
//
// Fetching includes waiting for authorization or for retry, waiting to be allowed by the
// service object to start the request, and actually fetching the request.
- (BOOL)isFetching {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return [self isFetchingUnsynchronized];
  }
}

- (BOOL)isFetchingUnsynchronized {
  GTMSessionCheckSynchronized(self);

  BOOL hasBegun = (_initialBeginFetchDate != nil);
  return hasBegun && !_hasStoppedFetching;
}

- (nullable NSURLResponse *)response {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    NSURLResponse *response = [self responseUnsynchronized];
    return response;
  }  // @synchronized(self)
}

- (nullable NSURLResponse *)responseUnsynchronized {
  GTMSessionCheckSynchronized(self);

  NSURLResponse *response = _sessionTask.response;
  if (!response) response = _response;
  return response;
}

- (NSInteger)statusCode {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    NSInteger statusCode = [self statusCodeUnsynchronized];
    return statusCode;
  }  // @synchronized(self)
}

- (NSInteger)statusCodeUnsynchronized {
  GTMSessionCheckSynchronized(self);

  NSURLResponse *response = [self responseUnsynchronized];
  NSInteger statusCode;

  if ([response respondsToSelector:@selector(statusCode)]) {
    statusCode = [(NSHTTPURLResponse *)response statusCode];
  } else {
    //  Default to zero, in hopes of hinting "Unknown" (we can't be
    //  sure that things are OK enough to use 200).
    statusCode = 0;
  }
  return statusCode;
}

- (nullable NSDictionary *)responseHeaders {
  GTMSessionCheckNotSynchronized(self);

  NSURLResponse *response = self.response;
  if ([response respondsToSelector:@selector(allHeaderFields)]) {
    NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
    return headers;
  }
  return nil;
}

- (nullable NSDictionary *)responseHeadersUnsynchronized {
  GTMSessionCheckSynchronized(self);

  NSURLResponse *response = [self responseUnsynchronized];
  if ([response respondsToSelector:@selector(allHeaderFields)]) {
    NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
    return headers;
  }
  return nil;
}

- (void)releaseCallbacks {
  // The clang included with Xcode 13.3 betas added a -Wunused-but-set-variable warning,
  // which doesn't (yet) skip variables annotated with objc_precie_lifetime. Since that
  // warning is not available in all Xcodes, turn off the -Wunused warning group entirely.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused"
  // Avoid releasing blocks in the sync section since objects dealloc'd by
  // the blocks being released may call back into the fetcher or fetcher
  // service.
  dispatch_queue_t NS_VALID_UNTIL_END_OF_SCOPE holdCallbackQueue;
  GTMSessionFetcherCompletionHandler NS_VALID_UNTIL_END_OF_SCOPE holdCompletionHandler;
#pragma clang diagnostic pop
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    holdCallbackQueue = _callbackQueue;
    holdCompletionHandler = _completionHandler;

    _callbackQueue = nil;
    _completionHandler = nil;  // Setter overridden in upload. Setter assumed to be used externally.
  }

  // Set local callback pointers to nil here rather than let them release at the end of the scope
  // to make any problems due to the blocks being released be a bit more obvious in a stack trace.
  holdCallbackQueue = nil;
  holdCompletionHandler = nil;

  self.configurationBlock = nil;
  self.didReceiveResponseBlock = nil;
  self.challengeBlock = nil;
  self.willRedirectBlock = nil;
  self.sendProgressBlock = nil;
  self.receivedProgressBlock = nil;
  self.downloadProgressBlock = nil;
  self.accumulateDataBlock = nil;
  self.willCacheURLResponseBlock = nil;
  self.retryBlock = nil;
  self.testBlock = nil;
  self.resumeDataBlock = nil;
  self.metricsCollectionBlock = nil;
}

- (void)forgetSessionIdentifierForFetcher {
  GTMSessionCheckSynchronized(self);
  [self forgetSessionIdentifierForFetcherWithoutSyncCheck];
}

- (void)forgetSessionIdentifierForFetcherWithoutSyncCheck {
  // This should be called inside a @synchronized block (except during dealloc.)
  if (_sessionIdentifier) {
    NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIdentifierToFetcherMap];
    [sessionIdentifierToFetcherMap removeObjectForKey:_sessionIdentifier];
    _sessionIdentifier = nil;
    _didCreateSessionIdentifier = NO;
  }
}

// External stop method
- (void)stopFetching {
  BOOL triggerCallback;
  BOOL inBeginFetch;
  BOOL stopTriggersHandler;
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);
    stopTriggersHandler = self.stopFetchingTriggersCompletionHandler;

    // Prevent enqueued callbacks from executing. The completion handler will still execute if
    // the property `stopFetchingTriggersCompletionHandler` is `YES`.
    _userStoppedFetching = YES;

    // Some of the delayed states want the complition to be triggered from here so they don't have
    // to do it during their flows for async work.
    triggerCallback = (_startingState == kStartingStateServiceDelayed ||
                       _startingState == kStartingStateAuthorizing ||
                       _startingState == kStartingStateCalculatingUA) &&
                      stopTriggersHandler;

    // If literally in `-beginFetchMayDelay:mayAuthorize:mayDecorate:`, it will handle the work to
    // stop.
    inBeginFetch = _startingState == kStartingStateStartingUp;

  }  // @synchronized(self)

  if (triggerCallback) {
    NSError *error = [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
                                         code:GTMSessionFetcherErrorUserCancelled
                                     userInfo:nil];
    [self failToBeginFetchWithError:error];
  } else if (!inBeginFetch) {
    [self stopFetchReleasingCallbacks:!stopTriggersHandler];
  }
}

// Cancel the fetch of the URL that's currently in progress.
//
// If shouldReleaseCallbacks is NO then the fetch will be retried so the callbacks need
// still be retained or `stopFetching` was called and `stopFetchingTriggersCompletionHandler` is
// `YES`.
- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks {
  [self removePersistedBackgroundSessionFromDefaults];

  GTMSessionFetcherService *service;
  NSMutableURLRequest *requestForStopAuth;

  // If the task or the retry timer is all that's retaining the fetcher,
  // we want to be sure this instance survives stopping at least long enough for
  // the stack to unwind.
  __autoreleasing GTMSessionFetcher *holdSelf = self;

  BOOL hasCanceledTask = NO;

  [holdSelf destroyRetryTimer];

  BOOL sendStopNotification = YES;
  BOOL callbacksPending = NO;
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _hasStoppedFetching = YES;

    service = _service;
    // Only will need to stop authorization if in an authorizing state.
    requestForStopAuth = (_startingState == kStartingStateAuthorizing) ? _request : nil;

    if (_sessionTask) {
      // In case cancelling the task or session calls this recursively, we want
      // to ensure that we'll only release the task and delegate once,
      // so first set _sessionTask to nil
      //
      // This may be called in a callback from the task, so use autorelease to avoid
      // releasing the task in its own callback.
      __autoreleasing NSURLSessionTask *oldTask = _sessionTask;
      if (!_isUsingTestBlock) {
        _response = _sessionTask.response;
      }
      _sessionTask = nil;

      if ([oldTask state] != NSURLSessionTaskStateCompleted) {
        // For download tasks, when the fetch is stopped, we may provide resume data that can
        // be used to create a new session.
        BOOL mayResume = (_resumeDataBlock &&
                          [oldTask respondsToSelector:@selector(cancelByProducingResumeData:)]);
        if (!mayResume) {
          [oldTask cancel];
          // A side effect of stopping the task is that URLSession:task:didCompleteWithError:
          // will be invoked asynchronously on the delegate queue.
        } else {
          void (^resumeBlock)(NSData *) = _resumeDataBlock;
          _resumeDataBlock = nil;

          // Save callbackQueue since releaseCallbacks clears it.
          dispatch_queue_t callbackQueue = _callbackQueue;
          dispatch_group_enter(_callbackGroup);
          [(NSURLSessionDownloadTask *)oldTask cancelByProducingResumeData:^(NSData *resumeData) {
            [self invokeOnCallbackQueue:callbackQueue
                       afterUserStopped:YES
                                  block:^{
                                    resumeBlock(resumeData);
                                    dispatch_group_leave(self->_callbackGroup);
                                  }];
          }];
        }
        hasCanceledTask = YES;
      }
    }

    // If the task was canceled, wait until the URLSession:task:didCompleteWithError: to call
    // finishTasksAndInvalidate, since calling it immediately tends to crash, see radar 18471901.
    if (_session) {
      BOOL shouldInvalidate = _shouldInvalidateSession;
#if TARGET_OS_IPHONE
      // Don't invalidate if we've got a systemCompletionHandler, since
      // URLSessionDidFinishEventsForBackgroundURLSession: won't be called if invalidated.
      shouldInvalidate = shouldInvalidate && !self.systemCompletionHandler;
#endif
      if (shouldInvalidate) {
        __autoreleasing NSURLSession *oldSession = _session;
        _session = nil;

        if (!hasCanceledTask) {
          [oldSession finishTasksAndInvalidate];
        } else {
          sendStopNotification = NO;
          _sessionNeedingInvalidation = oldSession;
        }
      }
    }
    callbacksPending = _stopFetchingTriggersCompletionHandler && _userStoppedFetching;
  }  // @synchronized(self)

  // If the NSURLSession needs to be invalidated, but needs to wait until the delegate method
  // URLSession:task:didCompleteWithError: is called, delay sending the fetch stopped notification
  // until then; otherwise send it now.
  if (sendStopNotification) {
    [self sendStopNotificationIfNeeded];
  }

  if (requestForStopAuth) {
    [_authorizer stopAuthorizationForRequest:requestForStopAuth];
  }

  if (shouldReleaseCallbacks) {
    [self releaseCallbacks];

    self.authorizer = nil;
  }

  [service fetcherDidStop:self callbacksPending:callbacksPending];

#if GTM_BACKGROUND_TASK_FETCHING
  [self endBackgroundTask];
#endif
}

- (void)setStopNotificationNeeded:(BOOL)flag {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _isStopNotificationNeeded = flag;
  }  // @synchronized(self)
}

- (void)sendStopNotificationIfNeeded {
  BOOL sendNow = NO;
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (_isStopNotificationNeeded) {
      _isStopNotificationNeeded = NO;
      sendNow = YES;
    }
  }  // @synchronized(self)

  if (sendNow) {
    [self postNotificationOnMainThreadWithName:kGTMSessionFetcherStoppedNotification userInfo:nil];
  }
}

- (void)retryFetch {
  [self stopFetchReleasingCallbacks:NO];

  // A retry will need a configuration with a fresh session identifier.
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (_sessionIdentifier && _didCreateSessionIdentifier) {
      [self forgetSessionIdentifierForFetcher];
      _configuration = nil;
    }

    if (_canShareSession) {
      // Force a grab of the current session from the fetcher service in case
      // the service's old one has become invalid.
      _session = nil;
    }
  }  // @synchronized(self)

  [self beginFetchForRetry];
}

- (BOOL)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds {
  // Uncovered in upload fetcher testing, because the chunk fetcher is being waited on, and gets
  // released by the upload code. The uploader just holds onto it with an ivar, and that gets
  // nilled in the chunk fetcher callback.
  // Used once in while loop just to avoid unused variable compiler warning.
  __autoreleasing GTMSessionFetcher *holdSelf = self;

  NSDate *giveUpDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds];

  BOOL shouldSpinRunLoop =
      ([NSThread isMainThread] &&
       (!self.callbackQueue || self.callbackQueue == dispatch_get_main_queue()));
  BOOL expired = NO;

  // Loop until the callbacks have been called and released, and until
  // the connection is no longer pending, until there are no callback dispatches
  // in flight, or until the timeout has expired.
  int64_t delta = (int64_t)(100 * NSEC_PER_MSEC);  // 100 ms
  while (1) {
    BOOL isTaskInProgress =
        (holdSelf->_sessionTask && [_sessionTask state] != NSURLSessionTaskStateCompleted);
    BOOL needsToCallCompletion = (_completionHandler != nil);
    BOOL isCallbackInProgress =
        (_callbackGroup &&
         dispatch_group_wait(_callbackGroup, dispatch_time(DISPATCH_TIME_NOW, delta)));

    if (!isTaskInProgress && !needsToCallCompletion && !isCallbackInProgress) break;

    expired = ([giveUpDate timeIntervalSinceNow] < 0);
    if (expired) {
      GTMSESSION_LOG_DEBUG(@"GTMSessionFetcher waitForCompletionWithTimeout:%0.1f expired -- "
                           @"%@%@%@",
                           timeoutInSeconds, isTaskInProgress ? @"taskInProgress " : @"",
                           needsToCallCompletion ? @"needsToCallCompletion " : @"",
                           isCallbackInProgress ? @"isCallbackInProgress" : @"");
      break;
    }

    // Run the current run loop 1/1000 of a second to give the networking
    // code a chance to work
    const NSTimeInterval kSpinInterval = 0.001;
    if (shouldSpinRunLoop) {
      NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:kSpinInterval];
      [[NSRunLoop currentRunLoop] runUntilDate:stopDate];
    } else {
      [NSThread sleepForTimeInterval:kSpinInterval];
    }
  }
  return !expired;
}

+ (void)setGlobalTestBlock:(nullable GTMSessionFetcherTestBlock)block {
#if GTM_DISABLE_FETCHER_TEST_BLOCK
  GTMSESSION_ASSERT_DEBUG(block == nil, @"test blocks disabled");
#endif
  gGlobalTestBlock = [block copy];
}

#if GTM_BACKGROUND_TASK_FETCHING

static _Nullable id<GTMUIApplicationProtocol> gSubstituteUIApp;

+ (void)setSubstituteUIApplication:(nullable id<GTMUIApplicationProtocol>)app {
  gSubstituteUIApp = app;
}

+ (nullable id<GTMUIApplicationProtocol>)substituteUIApplication {
  return gSubstituteUIApp;
}

+ (nullable id<GTMUIApplicationProtocol>)fetcherUIApplication {
  id<GTMUIApplicationProtocol> app = gSubstituteUIApp;
  if (app) return app;

  // iOS App extensions should not call [UIApplication sharedApplication], even
  // if UIApplication responds to it.

  static Class applicationClass = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    BOOL isAppExtension = [[[NSBundle mainBundle] bundlePath] hasSuffix:@".appex"];
    if (!isAppExtension) {
      Class cls = NSClassFromString(@"UIApplication");
      if (cls && [cls respondsToSelector:NSSelectorFromString(@"sharedApplication")]) {
        applicationClass = cls;
      }
    }
  });

  if (applicationClass) {
    app = (id<GTMUIApplicationProtocol>)[applicationClass sharedApplication];
  }
  return app;
}
#endif  //  GTM_BACKGROUND_TASK_FETCHING

#pragma mark NSURLSession Delegate Methods

// NSURLSession documentation indicates that redirectRequest can be passed to the handler
// but empirically redirectRequest lacks the HTTP body, so passing it will break POSTs.
// Instead, we construct a new request, a copy of the original, with overrides from the
// redirect.

- (void)URLSession:(NSURLSession *)session
                          task:(NSURLSessionTask *)task
    willPerformHTTPRedirection:(NSHTTPURLResponse *)redirectResponse
                    newRequest:(NSURLRequest *)redirectRequest
             completionHandler:(void (^)(NSURLRequest *_Nullable))handler {
  [self setSessionTask:task];
  GTMSESSION_LOG_DEBUG_VERBOSE(
      @"%@ %p URLSession:%@ task:%@ willPerformHTTPRedirection:%@ newRequest:%@", [self class],
      self, session, task, redirectResponse, redirectRequest);

  if ([self userStoppedFetching]) {
    handler(nil);
    return;
  }
  if (redirectRequest && redirectResponse) {
    // Copy the original request, including the body.
    NSURLRequest *originalRequest = self.request;
    NSMutableURLRequest *newRequest = [originalRequest mutableCopy];

    // The new requests's URL overrides the original's URL.
    [newRequest setURL:[GTMSessionFetcher redirectURLWithOriginalRequestURL:originalRequest.URL
                                                         redirectRequestURL:redirectRequest.URL]];

    // Any headers in the redirect override headers in the original.
    NSDictionary *redirectHeaders = redirectRequest.allHTTPHeaderFields;
    for (NSString *key in redirectHeaders) {
      NSString *value = [redirectHeaders objectForKey:key];
      [newRequest setValue:value forHTTPHeaderField:key];
    }

    redirectRequest = newRequest;

    // Log the response we just received
    [self setResponse:redirectResponse];
    [self logNowWithError:nil];

    GTMSessionFetcherWillRedirectBlock willRedirectBlock = self.willRedirectBlock;
    if (willRedirectBlock) {
      @synchronized(self) {
        GTMSessionMonitorSynchronized(self);
        __auto_type block = ^{
          willRedirectBlock(redirectResponse, redirectRequest, ^(NSURLRequest *clientRequest) {
            // Update the request for future logging.
            [self updateMutableRequest:[clientRequest mutableCopy]];

            handler(clientRequest);
          });
        };
        [self invokeOnCallbackQueueAfterUserStopped:YES block:block];
      }  // @synchronized(self)
      return;
    }
    // Continues here if the client did not provide a redirect block.

    // Update the request for future logging.
    [self updateMutableRequest:[redirectRequest mutableCopy]];
  }
  handler(redirectRequest);
}

- (void)URLSession:(NSURLSession *)session
              dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveResponse:(NSURLResponse *)response
     completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))handler {
  [self setSessionTask:dataTask];
  GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ dataTask:%@ didReceiveResponse:%@",
                               [self class], self, session, dataTask, response);
  __auto_type accumulateAndFinish = ^(NSURLSessionResponseDisposition dispositionValue) {
    // This method is called when the server has determined that it
    // has enough information to create the NSURLResponse
    // it can be called multiple times, for example in the case of a
    // redirect, so each time we reset the data.
    @synchronized(self) {
      GTMSessionMonitorSynchronized(self);

      BOOL hadPreviousData = self->_downloadedLength > 0;

      [self->_downloadedData setLength:0];
      self->_downloadedLength = 0;

      if (hadPreviousData && (dispositionValue != NSURLSessionResponseCancel)) {
        // Tell the accumulate block to discard prior data.
        GTMSessionFetcherAccumulateDataBlock accumulateBlock = self->_accumulateDataBlock;
        if (accumulateBlock) {
          [self invokeOnCallbackQueueUnlessStopped:^{
            accumulateBlock(nil);
          }];
        }
      }
    }  // @synchronized(self)
    handler(dispositionValue);
  };

  GTMSessionFetcherDidReceiveResponseBlock receivedResponseBlock;
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    receivedResponseBlock = _didReceiveResponseBlock;
    if (receivedResponseBlock) {
      // We will ultimately need to call back to NSURLSession's handler with the disposition value
      // for this delegate method even if the user has stopped the fetcher.
      __auto_type block = ^{
        receivedResponseBlock(response, ^(NSURLSessionResponseDisposition desiredDisposition) {
          accumulateAndFinish(desiredDisposition);
        });
      };
      [self invokeOnCallbackQueueAfterUserStopped:YES block:block];
    }
  }  // @synchronized(self)

  if (receivedResponseBlock == nil) {
    accumulateAndFinish(NSURLSessionResponseAllow);
  }
}

- (void)URLSession:(NSURLSession *)session
                 dataTask:(NSURLSessionDataTask *)dataTask
    didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask {
  GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ dataTask:%@ didBecomeDownloadTask:%@",
                               [self class], self, session, dataTask, downloadTask);
  [self setSessionTask:downloadTask];
}

- (void)URLSession:(NSURLSession *)session
                   task:(NSURLSessionTask *)task
    didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
      completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
                                  NSURLCredential *_Nullable credential))handler {
  [self setSessionTask:task];
  GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ task:%@ didReceiveChallenge:%@", [self class],
                               self, session, task, challenge);

  GTMSessionFetcherChallengeBlock challengeBlock = self.challengeBlock;
  if (challengeBlock) {
    // The fetcher user has provided custom challenge handling.
    //
    // We will ultimately need to call back to NSURLSession's handler with the disposition value
    // for this delegate method even if the user has stopped the fetcher.
    @synchronized(self) {
      GTMSessionMonitorSynchronized(self);

      [self invokeOnCallbackQueueAfterUserStopped:YES
                                            block:^{
                                              challengeBlock(self, challenge, handler);
                                            }];
    }
  } else {
    // No challenge block was provided by the client.
    [self respondToChallenge:challenge completionHandler:handler];
  }
}

- (void)respondToChallenge:(NSURLAuthenticationChallenge *)challenge
         completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
                                     NSURLCredential *_Nullable credential))handler {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    NSInteger previousFailureCount = [challenge previousFailureCount];
    if (previousFailureCount <= 2) {
      NSURLProtectionSpace *protectionSpace = [challenge protectionSpace];
      NSString *authenticationMethod = [protectionSpace authenticationMethod];
      if ([authenticationMethod isEqual:NSURLAuthenticationMethodServerTrust]) {
        // SSL.
        //
        // Background sessions seem to require an explicit check of the server trust object
        // rather than default handling.
        SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
        if (serverTrust == NULL) {
          // No server trust information is available.
          handler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
        } else {
          // Server trust information is available.
          __auto_type callback = ^(SecTrustRef trustRef, BOOL allow) {
            if (allow) {
              NSURLCredential *trustCredential = [NSURLCredential credentialForTrust:trustRef];
              handler(NSURLSessionAuthChallengeUseCredential, trustCredential);
            } else {
              GTMSESSION_LOG_DEBUG(@"Cancelling authentication challenge for %@",
                                   self->_request.URL);
              handler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
            }
          };
          if (_allowInvalidServerCertificates) {
            callback(serverTrust, YES);
          } else {
            [[self class] evaluateServerTrust:serverTrust
                                   forRequest:_request
                            completionHandler:callback];
          }
        }
        return;
      }

      NSURLCredential *credential = _credential;

      if ([[challenge protectionSpace] isProxy] && _proxyCredential != nil) {
        credential = _proxyCredential;
      }

      if (credential) {
        handler(NSURLSessionAuthChallengeUseCredential, credential);
      } else {
        // The credential is still nil; tell the OS to use the default handling. This is needed
        // for things that can come out of the keychain (proxies, client certificates, etc.).
        //
        // Note: Looking up a credential with NSURLCredentialStorage's
        // defaultCredentialForProtectionSpace: is *not* the same invoking the handler with
        // NSURLSessionAuthChallengePerformDefaultHandling. In the case of
        // NSURLAuthenticationMethodClientCertificate, you can get nil back from
        // NSURLCredentialStorage, while using this code path instead works.
        handler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
      }

    } else {
      // We've failed auth 3 times.  The completion handler will be called with code
      // NSURLErrorCancelled.
      handler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
    }
  }  // @synchronized(self)
}

// Return redirect URL based on the original request URL and redirect request URL.
//
// Method disallows any scheme changes between the original request URL and redirect request URL
// aside from "http" to "https". If a change in scheme is detected the redirect URL inherits the
// scheme from the original request URL.
+ (nullable NSURL *)redirectURLWithOriginalRequestURL:(nullable NSURL *)originalRequestURL
                                   redirectRequestURL:(nullable NSURL *)redirectRequestURL {
  // In the case of an NSURLSession redirect, neither URL should ever be nil; as a sanity check
  // if either is nil return the other URL.
  if (!redirectRequestURL) return originalRequestURL;
  if (!originalRequestURL) return redirectRequestURL;

  NSString *originalScheme = originalRequestURL.scheme;
  NSString *redirectScheme = redirectRequestURL.scheme;

  // If no change in scheme with redirect, just return the redirect.
  if (originalScheme != nil && redirectScheme != nil &&
      [originalScheme caseInsensitiveCompare:redirectScheme] == NSOrderedSame) {
    return redirectRequestURL;
  }

  BOOL insecureToSecureRedirect =
      (originalScheme != nil && [originalScheme caseInsensitiveCompare:@"http"] == NSOrderedSame &&
       redirectScheme != nil && [redirectScheme caseInsensitiveCompare:@"https"] == NSOrderedSame);

  // This can't really be nil for the inputs, but to keep the analyzer happy
  // for the -caseInsensitiveCompare: call below, give it a value if it were.
  if (!originalScheme) originalScheme = @"https";

  // Check for changes to the scheme and disallow any changes except for http to https.
  if (!insecureToSecureRedirect &&
      (redirectScheme.length != originalScheme.length ||
       [redirectScheme caseInsensitiveCompare:originalScheme] != NSOrderedSame)) {
    NSURLComponents *components =
        [NSURLComponents componentsWithURL:(NSURL *_Nonnull)redirectRequestURL
                   resolvingAgainstBaseURL:NO];
    components.scheme = originalScheme;
    return components.URL;
  }

  return redirectRequestURL;
}

// Validate the certificate chain.
//
// This may become a public method if it appears to be useful to users.
+ (void)evaluateServerTrust:(SecTrustRef)serverTrust
                 forRequest:(NSURLRequest *)request
          completionHandler:(void (^)(SecTrustRef trustRef, BOOL allow))handler {
  // Retain the trust object to avoid a SecTrustEvaluate() crash on iOS 7.
  CFRetain(serverTrust);

  // Evaluate the certificate chain.
  //
  // The delegate queue may be the main thread. Trust evaluation could cause some
  // blocking network activity, so we must evaluate async, as documented at
  // https://developer.apple.com/library/ios/technotes/tn2232/
  //
  // We must also avoid multiple uses of the trust object, per docs:
  // "It is not safe to call this function concurrently with any other function that uses
  // the same trust management object, or to re-enter this function for the same trust
  // management object."
  //
  // SecTrustEvaluateAsync both does sync execution of Evaluate and calls back on the
  // queue passed to it, according to at sources in
  // http://www.opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-55050.9/lib/SecTrust.cpp
  // It would require a global serial queue to ensure the evaluate happens only on a
  // single thread at a time, so we'll stick with using SecTrustEvaluate on a background
  // thread.
  dispatch_queue_t evaluateBackgroundQueue =
      dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  dispatch_async(evaluateBackgroundQueue, ^{
    // It looks like the implementation of SecTrustEvaluate() on Mac grabs a global lock,
    // so it may be redundant for us to also lock, but it's easy to synchronize here
    // anyway.
    BOOL shouldAllow;
    CFErrorRef errorRef = NULL;
    @synchronized([GTMSessionFetcher class]) {
      GTMSessionMonitorSynchronized([GTMSessionFetcher class]);

      // SecTrustEvaluateWithError handles both the "proceed" and "unspecified" cases,
      // so it is not necessary to check the trust result the evaluation returns true.
      shouldAllow = SecTrustEvaluateWithError(serverTrust, &errorRef);
    }

    if (errorRef) {
      GTMSESSION_LOG_DEBUG(@"Error %d evaluating trust for %@", (int)CFErrorGetCode(errorRef),
                           request);
      CFRelease(errorRef);
    }
    handler(serverTrust, shouldAllow);

    CFRelease(serverTrust);
  });
}

- (void)invokeOnCallbackQueueUnlessStopped:(void (^)(void))block {
  [self invokeOnCallbackQueueAfterUserStopped:NO block:block];
}

- (void)invokeOnCallbackQueueAfterUserStopped:(BOOL)afterStopped block:(void (^)(void))block {
  GTMSessionCheckSynchronized(self);

  [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:afterStopped block:block];
}

- (void)invokeOnCallbackUnsynchronizedQueueAfterUserStopped:(BOOL)afterStopped
                                                      block:(void (^)(void))block {
  // testBlock simulation code may not be synchronizing when this is invoked.
  [self invokeOnCallbackQueue:_callbackQueue afterUserStopped:afterStopped block:block];
}

- (void)invokeOnCallbackQueue:(dispatch_queue_t)callbackQueue
             afterUserStopped:(BOOL)afterStopped
                        block:(void (^)(void))block {
  if (callbackQueue) {
    dispatch_group_async(_callbackGroup, callbackQueue, ^{
      if (!afterStopped && !self->_stopFetchingTriggersCompletionHandler) {
        NSDate *serviceStoppedAllDate = [self->_service stoppedAllFetchersDate];

        @synchronized(self) {
          GTMSessionMonitorSynchronized(self);

          // Avoid a race between stopFetching and the callback.
          if (self->_userStoppedFetching) {
            return;
          }

          // Also avoid calling back if the service has stopped all fetchers
          // since this one was created. The fetcher may have stopped before
          // stopAllFetchers was invoked, so _userStoppedFetching wasn't set,
          // but the app still won't expect the callback to fire after
          // the service's stopAllFetchers was invoked.
          if (serviceStoppedAllDate &&
              [self->_initialBeginFetchDate compare:serviceStoppedAllDate] != NSOrderedDescending) {
            // stopAllFetchers was called after this fetcher began.
            return;
          }
        }  // @synchronized(self)
      }
      block();
    });
  }
}

- (void)invokeFetchCallbacksOnCallbackQueueWithData:(nullable NSData *)data
                                              error:(nullable NSError *)error
                                        mayDecorate:(BOOL)mayDecorate
                             shouldReleaseCallbacks:(BOOL)shouldReleaseCallbacks {
  if (mayDecorate && [_service respondsToSelector:@selector(decorators)]) {
    NSArray<id<GTMFetcherDecoratorProtocol>> *decorators = _service.decorators;
    if (decorators.count) {
      [self applyDecoratorsAtRequestDidFinish:decorators
                                     withData:data
                                        error:error
                              startingAtIndex:0
                       shouldReleaseCallbacks:shouldReleaseCallbacks];
      return;
    }
  }

  GTMSESSION_LOG_DEBUG_VERBOSE(
      @"GTMSessionFetcher invoking fetch callbacks, data length %lu, error %@",
      (unsigned long)data.length, error);

  // Callbacks will be released in the method stopFetchReleasingCallbacks:
  GTMSessionFetcherCompletionHandler handler;
  dispatch_queue_t callbackQueue;
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    // Capture the completion handler and callback queue, and call them only
    // after releasing any callbacks to ensure the order of release vs
    // callback is deterministic given potential QoS differences between
    // callback queue and whatever queue this method eventually executes on.
    handler = _completionHandler;
    callbackQueue = _callbackQueue;
  }

  if (shouldReleaseCallbacks) {
    [self releaseCallbacks];
  }

  if (handler) {
    [self invokeOnCallbackQueue:callbackQueue
               afterUserStopped:NO
                          block:^{
                            handler(data, error);

                            // Post a notification, primarily to allow code to collect responses for
                            // testing.
                            //
                            // The observing code is not likely on the fetcher's callback
                            // queue, so this posts explicitly to the main queue.
                            NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
                            if (data) {
                              userInfo[kGTMSessionFetcherCompletionDataKey] = data;
                            }
                            if (error) {
                              userInfo[kGTMSessionFetcherCompletionErrorKey] = error;
                            }
                            [self postNotificationOnMainThreadWithName:
                                      kGTMSessionFetcherCompletionInvokedNotification
                                                              userInfo:userInfo];
                          }];
  }
}

- (void)postNotificationOnMainThreadWithName:(NSString *)noteName
                                    userInfo:(nullable NSDictionary *)userInfo {
  // Historically, the notification has been posted immediately when on the main thread already, so
  // that is continued to not break any existing code. However, if a notification gets deferred to
  // be posted on the main thread, then even if some other code is one the main thread, it can't
  // post it's notification before the deferred one is posted, otherwise you could get a stop and/or
  // callback notification *before* the start notification. So if anything gets deferred to the main
  // thread, an immediated post on the main thread also has to get delayed to maintain the order.
  //
  // This could get revisited to defer all notifications, but that should probably go out in a
  // major version bump as it could be breaking to some usages who were dependent on the ordering.

  GTMSessionCheckNotSynchronized(self);

  BOOL canPostNow = NO;
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (_pendingNotifications == 0 && [NSThread isMainThread]) {
      canPostNow = YES;
    } else {
      // This one will get deferred, so increase the counter for deferred notifications.
      _pendingNotifications++;
    }
  }
  if (canPostNow) {
    [[NSNotificationCenter defaultCenter] postNotificationName:noteName
                                                        object:self
                                                      userInfo:userInfo];
  } else {
    dispatch_async(dispatch_get_main_queue(), ^{
      @synchronized(self) {
        GTMSessionMonitorSynchronized(self);

        GTMSESSION_ASSERT_DEBUG(
            self->_pendingNotifications > 0,
            @"Internal error: firing pending notification when wasn't tracked: %lu",
            (unsigned long)self->_pendingNotifications);
        self->_pendingNotifications--;
      }

      [[NSNotificationCenter defaultCenter] postNotificationName:noteName
                                                          object:self
                                                        userInfo:userInfo];
    });
  }
}

- (void)URLSession:(NSURLSession *)session
                 task:(NSURLSessionTask *)uploadTask
    needNewBodyStream:(void (^)(NSInputStream *_Nullable bodyStream))completionHandler {
  [self setSessionTask:uploadTask];
  GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ task:%@ needNewBodyStream:", [self class],
                               self, session, uploadTask);
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    GTMSessionFetcherBodyStreamProvider provider = _bodyStreamProvider;
#if !STRIP_GTM_FETCH_LOGGING
    if ([self respondsToSelector:@selector(loggedStreamProviderForStreamProvider:)]) {
      provider = [self performSelector:@selector(loggedStreamProviderForStreamProvider:)
                            withObject:provider];
    }
#endif
    if (provider) {
      [self invokeOnCallbackQueueUnlessStopped:^{
        provider(completionHandler);
      }];
    } else {
      GTMSESSION_ASSERT_DEBUG(NO, @"NSURLSession expects a stream provider");

      completionHandler(nil);
    }
  }  // @synchronized(self)
}

- (void)URLSession:(NSURLSession *)session
                        task:(NSURLSessionTask *)task
             didSendBodyData:(int64_t)bytesSent
              totalBytesSent:(int64_t)totalBytesSent
    totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
  [self setSessionTask:task];
  GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ task:%@ didSendBodyData:%lld"
                               @" totalBytesSent:%lld totalBytesExpectedToSend:%lld",
                               [self class], self, session, task, bytesSent, totalBytesSent,
                               totalBytesExpectedToSend);
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (!_sendProgressBlock) {
      return;
    }
    // We won't hold on to send progress block; it's ok to not send it if the upload finishes.
    [self invokeOnCallbackQueueUnlessStopped:^{
      GTMSessionFetcherSendProgressBlock progressBlock;
      @synchronized(self) {
        GTMSessionMonitorSynchronized(self);

        progressBlock = self->_sendProgressBlock;
      }
      if (progressBlock) {
        progressBlock(bytesSent, totalBytesSent, totalBytesExpectedToSend);
      }
    }];
  }  // @synchronized(self)
}

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data {
  [self setSessionTask:dataTask];
  NSUInteger bufferLength = data.length;
  GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ dataTask:%@ didReceiveData:%p (%llu bytes)",
                               [self class], self, session, dataTask, data,
                               (unsigned long long)bufferLength);
  if (bufferLength == 0) {
    // Observed on completing an out-of-process upload.
    return;
  }
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    GTMSessionFetcherAccumulateDataBlock accumulateBlock = _accumulateDataBlock;
    if (accumulateBlock) {
      // Let the client accumulate the data.
      _downloadedLength += bufferLength;
      [self invokeOnCallbackQueueUnlessStopped:^{
        accumulateBlock(data);
      }];
    } else if (!_userStoppedFetching) {
      // Append to the mutable data buffer unless the fetch has been cancelled.

      // Resumed upload tasks may not yet have a data buffer.
      if (_downloadedData == nil) {
        // Using NSClassFromString for iOS 6 compatibility.
        GTMSESSION_ASSERT_DEBUG(
            ![dataTask isKindOfClass:NSClassFromString(@"NSURLSessionDownloadTask")],
            @"Resumed download tasks should not receive data bytes");
        _downloadedData = [[NSMutableData alloc] init];
      }

      [_downloadedData appendData:data];
      _downloadedLength = (int64_t)_downloadedData.length;

      // We won't hold on to receivedProgressBlock here; it's ok to not send
      // it if the transfer finishes.
      if (_receivedProgressBlock) {
        [self invokeOnCallbackQueueUnlessStopped:^{
          GTMSessionFetcherReceivedProgressBlock progressBlock;
          int64_t downloadedLength;
          @synchronized(self) {
            GTMSessionMonitorSynchronized(self);

            progressBlock = self->_receivedProgressBlock;
            downloadedLength = self->_downloadedLength;
          }
          if (progressBlock) {
            progressBlock((int64_t)bufferLength, downloadedLength);
          }
        }];
      }
    }
  }  // @synchronized(self)
}

- (void)URLSession:(NSURLSession *)session
             dataTask:(NSURLSessionDataTask *)dataTask
    willCacheResponse:(NSCachedURLResponse *)proposedResponse
    completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
    GTM_SWIFT_DISABLE_ASYNC {
  GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ dataTask:%@ willCacheResponse:%@ %@",
                               [self class], self, session, dataTask, proposedResponse,
                               proposedResponse.response);
  GTMSessionFetcherWillCacheURLResponseBlock callback;
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    callback = _willCacheURLResponseBlock;

    if (callback) {
      [self invokeOnCallbackQueueAfterUserStopped:YES
                                            block:^{
                                              callback(proposedResponse, completionHandler);
                                            }];
    }
  }  // @synchronized(self)
  if (!callback) {
    completionHandler(proposedResponse);
  }
}

- (void)URLSession:(NSURLSession *)session
                 downloadTask:(NSURLSessionDownloadTask *)downloadTask
                 didWriteData:(int64_t)bytesWritten
            totalBytesWritten:(int64_t)totalBytesWritten
    totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
  GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ downloadTask:%@ didWriteData:%lld"
                               @" bytesWritten:%lld totalBytesExpectedToWrite:%lld",
                               [self class], self, session, downloadTask, bytesWritten,
                               totalBytesWritten, totalBytesExpectedToWrite);
  [self setSessionTask:downloadTask];
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if ((totalBytesExpectedToWrite != NSURLSessionTransferSizeUnknown) &&
        (totalBytesExpectedToWrite < totalBytesWritten)) {
      // Have observed cases were bytesWritten == totalBytesExpectedToWrite,
      // but totalBytesWritten > totalBytesExpectedToWrite, so setting to unkown in these cases.
      totalBytesExpectedToWrite = NSURLSessionTransferSizeUnknown;
    }

    GTMSessionFetcherDownloadProgressBlock progressBlock;
    progressBlock = self->_downloadProgressBlock;
    if (progressBlock) {
      [self invokeOnCallbackQueueUnlessStopped:^{
        progressBlock(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
      }];
    }
  }  // @synchronized(self)
}

- (void)URLSession:(NSURLSession *)session
          downloadTask:(NSURLSessionDownloadTask *)downloadTask
     didResumeAtOffset:(int64_t)fileOffset
    expectedTotalBytes:(int64_t)expectedTotalBytes {
  GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ downloadTask:%@ didResumeAtOffset:%lld"
                               @" expectedTotalBytes:%lld",
                               [self class], self, session, downloadTask, fileOffset,
                               expectedTotalBytes);
  [self setSessionTask:downloadTask];
}

- (void)URLSession:(NSURLSession *)session
                 downloadTask:(NSURLSessionDownloadTask *)downloadTask
    didFinishDownloadingToURL:(NSURL *)downloadLocationURL {
  // Download may have relaunched app, so update _sessionTask.
  [self setSessionTask:downloadTask];
  GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ downloadTask:%@ didFinishDownloadingToURL:%@",
                               [self class], self, session, downloadTask, downloadLocationURL);
  NSNumber *fileSizeNum;
  [downloadLocationURL getResourceValue:&fileSizeNum forKey:NSURLFileSizeKey error:NULL];
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    NSURL *destinationURL = _destinationFileURL;

    _downloadedLength = fileSizeNum.longLongValue;

    // Overwrite any previous file at the destination URL.
    NSFileManager *fileMgr = [NSFileManager defaultManager];
    NSError *removeError;
    if (![fileMgr removeItemAtURL:destinationURL error:&removeError] &&
        removeError.code != NSFileNoSuchFileError) {
      GTMSESSION_LOG_DEBUG(@"Could not remove previous file at %@ due to %@",
                           downloadLocationURL.path, removeError);
    }

    NSInteger statusCode = [self statusCodeUnsynchronized];
    if (statusCode < 200 || statusCode > 399) {
      // In OS X 10.11, the response body is written to a file even on a server
      // status error.  For convenience of the fetcher client, we'll skip saving the
      // downloaded body to the destination URL so that clients do not need to know
      // to delete the file following fetch errors.
      GTMSESSION_LOG_DEBUG(@"Abandoning download due to status %ld, file %@", (long)statusCode,
                           downloadLocationURL.path);

      // On error code, add the contents of the temporary file to _downloadTaskErrorData
      // This way fetcher clients have access to error details possibly passed by the server.
      if (_downloadedLength > 0 && _downloadedLength <= kMaximumDownloadErrorDataLength) {
        _downloadTaskErrorData = [NSData dataWithContentsOfURL:downloadLocationURL];
      } else if (_downloadedLength > kMaximumDownloadErrorDataLength) {
        GTMSESSION_LOG_DEBUG(@"Download error data for file %@ not passed to userInfo due to size "
                             @"%lld",
                             downloadLocationURL.path, _downloadedLength);
      }
    } else {
      NSError *moveError;
      NSURL *destinationFolderURL = [destinationURL URLByDeletingLastPathComponent];
      BOOL didMoveDownload = NO;
      if ([fileMgr createDirectoryAtURL:destinationFolderURL
              withIntermediateDirectories:YES
                               attributes:nil
                                    error:&moveError]) {
        didMoveDownload = [fileMgr moveItemAtURL:downloadLocationURL
                                           toURL:destinationURL
                                           error:&moveError];
      }
      if (!didMoveDownload) {
        _downloadFinishedError = moveError;
      }
      GTM_LOG_BACKGROUND_SESSION(@"%@ %p Moved download from \"%@\" to \"%@\"  %@", [self class],
                                 self, downloadLocationURL.path, destinationURL.path,
                                 error ? error : @"");
    }
  }  // @synchronized(self)
}

/* Sent as the last message related to a specific task.  Error may be
 * nil, which implies that no error occurred and this task is complete.
 */
- (void)URLSession:(NSURLSession *)session
                    task:(NSURLSessionTask *)task
    didCompleteWithError:(NSError *)error {
  [self setSessionTask:task];
  GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ task:%@ didCompleteWithError:%@", [self class],
                               self, session, task, error);

  NSInteger status = self.statusCode;
  BOOL forceAssumeRetry = NO;
  BOOL succeeded = NO;
  BOOL userStoppedTriggerCompletion = NO;
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

#if !GTM_DISABLE_FETCHER_TEST_BLOCK
    // The task is never resumed when a testBlock is used. When the session is destroyed,
    // we should ignore the callback, since the testBlock support code itself invokes
    // shouldRetryNowForStatus: and finishWithError:shouldRetry:
    if (_isUsingTestBlock) return;
#endif
    userStoppedTriggerCompletion = _userStoppedFetching && _stopFetchingTriggersCompletionHandler;

    if (error == nil) {
      error = _downloadFinishedError;
    }
    succeeded = (error == nil && status >= 0 && status < 300);
    if (succeeded && !userStoppedTriggerCompletion) {
      // Succeeded.
      _bodyLength = task.countOfBytesSent;
    }
  }  // @synchronized(self)

  if (userStoppedTriggerCompletion) {
    NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
    [userInfo setObject:@"Operation cancelled" forKey:NSLocalizedDescriptionKey];
    if (error) {
      [userInfo setObject:error forKey:NSUnderlyingErrorKey];
    }
    NSError *cancelError = [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
                                               code:GTMSessionFetcherErrorUserCancelled
                                           userInfo:userInfo];
    [self finishWithError:cancelError shouldRetry:NO];
    return;
  }

  if (succeeded) {
    [self finishWithError:nil shouldRetry:NO];
    return;
  }
  // For background redirects, no delegate method is called, so we cannot restore a stripped
  // Authorization header, so if a 403 ("Forbidden") was generated due to a missing OAuth 2 header,
  // set the current request's URL to the redirected URL, so we in effect restore the Authorization
  // header.
  if ((status == 403) && self.usingBackgroundSession) {
    NSURL *redirectURL = self.response.URL;
    NSURLRequest *request = self.request;
    if (![request.URL isEqual:redirectURL]) {
      NSString *authorizationHeader = [request.allHTTPHeaderFields objectForKey:@"Authorization"];
      if (authorizationHeader != nil) {
        NSMutableURLRequest *mutableRequest = [request mutableCopy];
        mutableRequest.URL = redirectURL;
        [self updateMutableRequest:mutableRequest];
        // Avoid assuming the session is still valid.
        self.session = nil;
        forceAssumeRetry = YES;
      }
    }
  }

  // If invalidating the session was deferred in stopFetchReleasingCallbacks: then do it now.
  NSURLSession *oldSession = self.sessionNeedingInvalidation;
  if (oldSession) {
    [self setSessionNeedingInvalidation:NULL];
    [oldSession finishTasksAndInvalidate];
  }

  // Failed.
  [self shouldRetryNowForStatus:status
                          error:error
               forceAssumeRetry:forceAssumeRetry
                       response:^(BOOL shouldRetry) {
                         [self finishWithError:error shouldRetry:shouldRetry];
                       }];
}

- (void)URLSession:(NSURLSession *)session
                          task:(NSURLSessionTask *)task
    didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);
    GTMSessionFetcherMetricsCollectionBlock metricsCollectionBlock = _metricsCollectionBlock;
    if (metricsCollectionBlock) {
      [self invokeOnCallbackQueueUnlessStopped:^{
        metricsCollectionBlock(metrics);
      }];
    }
  }
}

#if TARGET_OS_IPHONE
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
  GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSessionDidFinishEventsForBackgroundURLSession:%@",
                               [self class], self, session);
  [self removePersistedBackgroundSessionFromDefaults];

  GTMSessionFetcherSystemCompletionHandler handler;
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    handler = self.systemCompletionHandler;
    self.systemCompletionHandler = nil;
  }  // @synchronized(self)
  if (handler) {
    GTM_LOG_BACKGROUND_SESSION(@"%@ %p Calling system completionHandler", [self class], self);
    handler();

    @synchronized(self) {
      GTMSessionMonitorSynchronized(self);

      NSURLSession *oldSession = _session;
      _session = nil;
      if (_shouldInvalidateSession) {
        [oldSession finishTasksAndInvalidate];
      }
    }  // @synchronized(self)
  }
}
#endif

- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable NSError *)error {
  // This may happen repeatedly for retries.  On authentication callbacks, the retry
  // may begin before the prior session sends the didBecomeInvalid delegate message.
  GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ didBecomeInvalidWithError:%@", [self class],
                               self, session, error);
  if (session == (NSURLSession *)self.session) {
    GTMSESSION_LOG_DEBUG_VERBOSE(@"  Unexpected retained invalid session: %@", session);
    self.session = nil;
  }
}

- (void)finishWithError:(nullable NSError *)error shouldRetry:(BOOL)shouldRetry {
  [self removePersistedBackgroundSessionFromDefaults];

  BOOL shouldStopFetching = YES;
  NSData *downloadedData = nil;
#if !STRIP_GTM_FETCH_LOGGING
  BOOL shouldDeferLogging = NO;
#endif
  BOOL shouldBeginRetryTimer = NO;
  NSInteger status = [self statusCode];
  NSURL *destinationURL = self.destinationFileURL;

  BOOL fetchSucceeded = (error == nil && status >= 0 && status < 300);

#if !STRIP_GTM_FETCH_LOGGING
  if (!fetchSucceeded) {
    if (!shouldDeferLogging && !self.hasLoggedError) {
      [self logNowWithError:error];
      self.hasLoggedError = YES;
    }
  }
#endif  // !STRIP_GTM_FETCH_LOGGING

  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

#if !STRIP_GTM_FETCH_LOGGING
    shouldDeferLogging = _deferResponseBodyLogging;
#endif
    if (fetchSucceeded) {
      // Success
      if ((_downloadedData.length > 0) && (destinationURL != nil)) {
        // Overwrite any previous file at the destination URL.
        NSFileManager *fileMgr = [NSFileManager defaultManager];
        [fileMgr removeItemAtURL:destinationURL error:NULL];
        NSURL *destinationFolderURL = [destinationURL URLByDeletingLastPathComponent];
        BOOL didMoveDownload = NO;
        if ([fileMgr createDirectoryAtURL:destinationFolderURL
                withIntermediateDirectories:YES
                                 attributes:nil
                                      error:&error]) {
          didMoveDownload = [_downloadedData writeToURL:destinationURL
                                                options:NSDataWritingAtomic
                                                  error:&error];
        }
        if (didMoveDownload) {
          _downloadedData = nil;
        } else {
          _downloadFinishedError = error;
        }
      }
      downloadedData = _downloadedData;
    } else {
      // Unsuccessful with error or status over 300. Retry or notify the delegate of failure
      if (shouldRetry) {
        // Retrying.
        shouldBeginRetryTimer = YES;
        shouldStopFetching = NO;
      } else {
        if (error == nil) {
          // Create an error.
          NSDictionary *userInfo = GTMErrorUserInfoForData(
              _downloadedData.length > 0 ? _downloadedData : _downloadTaskErrorData,
              [self responseHeadersUnsynchronized]);

          error = [NSError errorWithDomain:kGTMSessionFetcherStatusDomain
                                      code:status
                                  userInfo:userInfo];
        } else {
          // If the error had resume data, and the client supplied a resume block, pass the
          // data to the client.
          void (^resumeBlock)(NSData *) = _resumeDataBlock;
          _resumeDataBlock = nil;
          if (resumeBlock) {
            NSData *resumeData = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData];
            if (resumeData) {
              [self invokeOnCallbackQueueAfterUserStopped:YES
                                                    block:^{
                                                      resumeBlock(resumeData);
                                                    }];
            }
          }
        }
        if (_downloadedData.length > 0) {
          downloadedData = _downloadedData;
        }
        // If the error occurred after retries, report the number and duration of the
        // retries. This provides a clue to a developer looking at the error description
        // that the fetcher did retry before failing with this error.
        if (_retryCount > 0) {
          NSMutableDictionary *userInfoWithRetries =
              [NSMutableDictionary dictionaryWithDictionary:(NSDictionary *)error.userInfo];
          NSTimeInterval timeSinceInitialRequest = -[_initialRequestDate timeIntervalSinceNow];
          [userInfoWithRetries setObject:@(timeSinceInitialRequest)
                                  forKey:kGTMSessionFetcherElapsedIntervalWithRetriesKey];
          [userInfoWithRetries setObject:@(_retryCount)
                                  forKey:kGTMSessionFetcherNumberOfRetriesDoneKey];
          error = [NSError errorWithDomain:(NSString *)error.domain
                                      code:error.code
                                  userInfo:userInfoWithRetries];
        }
      }
    }
  }  // @synchronized(self)

  if (shouldBeginRetryTimer) {
    [self beginRetryTimer];
  }

  // We want to send the stop notification before calling the delegate's
  // callback selector, since the callback selector may release all of
  // the fetcher properties that the client is using to track the fetches.
  //
  // We'll also stop now so that, to any observers watching the notifications,
  // it doesn't look like our wait for a retry (which may be long,
  // 30 seconds or more) is part of the network activity.
  [self sendStopNotificationIfNeeded];

  if (shouldStopFetching) {
    // The upload subclass doesn't want to release callbacks until upload chunks have completed.
    BOOL shouldRelease = [self shouldReleaseCallbacksUponCompletion];
    [self invokeFetchCallbacksOnCallbackQueueWithData:downloadedData
                                                error:error
                                          mayDecorate:YES
                               shouldReleaseCallbacks:shouldRelease];
    [self stopFetchReleasingCallbacks:NO];
  }

#if !STRIP_GTM_FETCH_LOGGING
  // _hasLoggedError is only set by this method
  if (!shouldDeferLogging && !_hasLoggedError) {
    [self logNowWithError:error];
  }
#endif
}

- (BOOL)shouldReleaseCallbacksUponCompletion {
  // A subclass can override this to keep callbacks around after the
  // connection has finished successfully
  return YES;
}

- (void)logNowWithError:(nullable NSError *)error {
  GTMSessionCheckNotSynchronized(self);

  // If the logging category is available, then log the current request,
  // response, data, and error
  if ([self respondsToSelector:@selector(logFetchWithError:)]) {
    [self performSelector:@selector(logFetchWithError:) withObject:error];
  }
}

#pragma mark Retries

- (BOOL)isRetryError:(NSError *)error {
  struct RetryRecord {
    __unsafe_unretained NSString *const domain;
    NSInteger code;
  };

  struct RetryRecord retries[] = {
      {kGTMSessionFetcherStatusDomain, 408},  // request timeout
      {kGTMSessionFetcherStatusDomain, 502},  // failure gatewaying to another server
      {kGTMSessionFetcherStatusDomain, 503},  // service unavailable
      {kGTMSessionFetcherStatusDomain, 504},  // request timeout
      {NSURLErrorDomain, NSURLErrorTimedOut},
      {NSURLErrorDomain, NSURLErrorNetworkConnectionLost},
      {nil, 0}};

  // NSError's isEqual always returns false for equal but distinct instances
  // of NSError, so we have to compare the domain and code values explicitly
  NSString *domain = error.domain;
  NSInteger code = error.code;
  for (int idx = 0; retries[idx].domain != nil; idx++) {
    if (code == retries[idx].code && [domain isEqual:retries[idx].domain]) {
      return YES;
    }
  }
  return NO;
}

// shouldRetryNowForStatus:error: responds with YES if the user has enabled retries
// and the status or error is one that is suitable for retrying.  "Suitable"
// means either the isRetryError:'s list contains the status or error, or the
// user's retry block is present and returns YES when called, or the
// authorizer may be able to fix.
- (void)shouldRetryNowForStatus:(NSInteger)status
                          error:(NSError *)error
               forceAssumeRetry:(BOOL)forceAssumeRetry
                       response:(GTMSessionFetcherRetryResponse)response {
  // Determine if a refreshed authorizer may avoid an authorization error
  BOOL willRetry = NO;

  // We assume _authorizer is immutable after beginFetch, and _hasAttemptedAuthRefresh is modified
  // only in this method, and this method is invoked on the serial delegate queue.
  //
  // We want to avoid calling the authorizer from inside a sync block.
  BOOL isFirstAuthError = (_authorizer != nil && !_hasAttemptedAuthRefresh &&
                           status == GTMSessionFetcherStatusUnauthorized);  // 401

  BOOL hasPrimed = NO;
  if (isFirstAuthError) {
    if ([_authorizer respondsToSelector:@selector(primeForRefresh)]) {
      hasPrimed = [_authorizer primeForRefresh];
    }
  }

  BOOL shouldRetryForAuthRefresh = NO;
  if (hasPrimed) {
    shouldRetryForAuthRefresh = YES;
    _hasAttemptedAuthRefresh = YES;
    [self updateRequestValue:nil forHTTPHeaderField:@"Authorization"];
  }

  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    BOOL shouldDoRetry = [self isRetryEnabledUnsynchronized];
    if (shouldDoRetry && ![self hasRetryAfterInterval]) {
      // Determine if we're doing exponential backoff retries
      shouldDoRetry = [self nextRetryIntervalUnsynchronized] < _maxRetryInterval;

      if (shouldDoRetry) {
        // If an explicit max retry interval was set, we expect repeated backoffs to take
        // up to roughly twice that for repeated fast failures.  If the initial attempt is
        // already more than 3 times the max retry interval, then failures have taken a long time
        // (such as from network timeouts) so don't retry again to avoid the app becoming
        // unexpectedly unresponsive.
        if (_maxRetryInterval > 0) {
          NSTimeInterval maxAllowedIntervalBeforeRetry = _maxRetryInterval * 3;
          NSTimeInterval timeSinceInitialRequest = -[_initialRequestDate timeIntervalSinceNow];
          if (timeSinceInitialRequest > maxAllowedIntervalBeforeRetry) {
            shouldDoRetry = NO;
          }
        }
      }
    }
    BOOL canRetry = shouldRetryForAuthRefresh || forceAssumeRetry || shouldDoRetry;
    if (canRetry) {
      NSDictionary *userInfo =
          GTMErrorUserInfoForData(_downloadedData, [self responseHeadersUnsynchronized]);
      NSError *statusError = [NSError errorWithDomain:kGTMSessionFetcherStatusDomain
                                                 code:status
                                             userInfo:userInfo];
      if (error == nil) {
        error = statusError;
      }
      willRetry = shouldRetryForAuthRefresh || forceAssumeRetry || [self isRetryError:error] ||
                  ((error != statusError) && [self isRetryError:statusError]);

      // If the user has installed a retry callback, consult that.
      GTMSessionFetcherRetryBlock retryBlock = _retryBlock;
      if (retryBlock) {
        [self invokeOnCallbackQueueUnlessStopped:^{
          retryBlock(willRetry, error, response);
        }];
        return;
      }
    }
  }  // @synchronized(self)
  response(willRetry);
}

- (BOOL)hasRetryAfterInterval {
  GTMSessionCheckSynchronized(self);

  NSDictionary *responseHeaders = [self responseHeadersUnsynchronized];
  NSString *retryAfterValue = [responseHeaders valueForKey:@"Retry-After"];
  return (retryAfterValue != nil);
}

- (NSTimeInterval)retryAfterInterval {
  GTMSessionCheckSynchronized(self);

  NSDictionary *responseHeaders = [self responseHeadersUnsynchronized];
  NSString *retryAfterValue = [responseHeaders valueForKey:@"Retry-After"];
  if (retryAfterValue == nil) {
    return 0;
  }
  // Retry-After formatted as HTTP-date | delta-seconds
  // Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
  NSDateFormatter *rfc1123DateFormatter = [[NSDateFormatter alloc] init];
  rfc1123DateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
  rfc1123DateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
  rfc1123DateFormatter.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss z";
  NSDate *retryAfterDate = [rfc1123DateFormatter dateFromString:retryAfterValue];
  NSTimeInterval retryAfterInterval =
      (retryAfterDate != nil) ? retryAfterDate.timeIntervalSinceNow : retryAfterValue.intValue;
  retryAfterInterval = MAX(0, retryAfterInterval);
  return retryAfterInterval;
}

- (void)beginRetryTimer {
  if (![NSThread isMainThread]) {
    // Defer creating and starting the timer until we're on the main thread to ensure it has
    // a run loop.
    dispatch_group_async(_callbackGroup, dispatch_get_main_queue(), ^{
      [self beginRetryTimer];
    });
    return;
  }

  [self destroyRetryTimer];

#if GTM_BACKGROUND_TASK_FETCHING
  // Don't keep a background task active while awaiting retry, which can lead to the
  // app exceeding the allotted time for keeping the background task open, causing the
  // system to terminate the app. When the retry starts, a new background task will
  // be created.
  [self endBackgroundTask];
#endif  // GTM_BACKGROUND_TASK_FETCHING

  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    NSTimeInterval nextInterval = [self nextRetryIntervalUnsynchronized];
    NSTimeInterval maxInterval = _maxRetryInterval;
    NSTimeInterval newInterval = MIN(nextInterval, (maxInterval > 0 ? maxInterval : DBL_MAX));
    NSTimeInterval newIntervalTolerance = (newInterval / 10) > 1.0 ?: 1.0;

    _lastRetryInterval = newInterval;

    _retryTimer = [NSTimer timerWithTimeInterval:newInterval
                                          target:self
                                        selector:@selector(retryTimerFired:)
                                        userInfo:nil
                                         repeats:NO];
    _retryTimer.tolerance = newIntervalTolerance;
    [[NSRunLoop mainRunLoop] addTimer:_retryTimer forMode:NSDefaultRunLoopMode];
  }  // @synchronized(self)

  [self postNotificationOnMainThreadWithName:kGTMSessionFetcherRetryDelayStartedNotification
                                    userInfo:nil];
}

- (void)retryTimerFired:(NSTimer *)timer {
  [self destroyRetryTimer];

  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _retryCount++;
  }  // @synchronized(self)

  NSOperationQueue *queue = self.sessionDelegateQueue;
  [queue addOperationWithBlock:^{
    [self retryFetch];
  }];
}

- (void)destroyRetryTimer {
  BOOL shouldNotify = NO;

  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (_retryTimer) {
      [_retryTimer invalidate];
      _retryTimer = nil;
      shouldNotify = YES;
    }
  }

  if (shouldNotify) {
    [self postNotificationOnMainThreadWithName:kGTMSessionFetcherRetryDelayStoppedNotification
                                      userInfo:nil];
  }
}

- (NSUInteger)retryCount {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _retryCount;
  }  // @synchronized(self)
}

- (NSTimeInterval)nextRetryInterval {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    NSTimeInterval interval = [self nextRetryIntervalUnsynchronized];
    return interval;
  }  // @synchronized(self)
}

- (NSTimeInterval)nextRetryIntervalUnsynchronized {
  GTMSessionCheckSynchronized(self);

  NSInteger statusCode = [self statusCodeUnsynchronized];
  if ((statusCode == 503) && [self hasRetryAfterInterval]) {
    NSTimeInterval secs = [self retryAfterInterval];
    return secs;
  }
  // The next wait interval is the factor (2.0) times the last interval,
  // but never less than the minimum interval.
  NSTimeInterval secs = _lastRetryInterval * _retryFactor;
  if (_maxRetryInterval > 0) {
    secs = MIN(secs, _maxRetryInterval);
  }
  secs = MAX(secs, _minRetryInterval);

  return secs;
}

- (NSTimer *)retryTimer {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _retryTimer;
  }  // @synchronized(self)
}

- (BOOL)isRetryEnabled {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _isRetryEnabled;
  }  // @synchronized(self)
}

- (BOOL)isRetryEnabledUnsynchronized {
  GTMSessionCheckSynchronized(self);

  return _isRetryEnabled;
}

- (void)setRetryEnabled:(BOOL)flag {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (flag && !_isRetryEnabled) {
      // We defer initializing these until the user calls setRetryEnabled
      // to avoid using the random number generator if it's not needed.
      // However, this means min and max intervals for this fetcher are reset
      // as a side effect of calling setRetryEnabled.
      //
      // Make an initial retry interval random between 1.0 and 2.0 seconds
      _minRetryInterval = InitialMinRetryInterval();
      _maxRetryInterval = kUnsetMaxRetryInterval;
      _retryFactor = 2.0;
      _lastRetryInterval = 0.0;
    }
    _isRetryEnabled = flag;
  }  // @synchronized(self)
};

- (NSTimeInterval)maxRetryInterval {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _maxRetryInterval;
  }  // @synchronized(self)
}

- (void)setMaxRetryInterval:(NSTimeInterval)secs {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (secs > 0) {
      _maxRetryInterval = secs;
    } else {
      _maxRetryInterval = kUnsetMaxRetryInterval;
    }
  }  // @synchronized(self)
}

- (double)minRetryInterval {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _minRetryInterval;
  }  // @synchronized(self)
}

- (void)setMinRetryInterval:(NSTimeInterval)secs {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (secs > 0) {
      _minRetryInterval = secs;
    } else {
      // Set min interval to a random value between 1.0 and 2.0 seconds
      // so that if multiple clients start retrying at the same time, they'll
      // repeat at different times and avoid overloading the server
      _minRetryInterval = InitialMinRetryInterval();
    }
  }  // @synchronized(self)
}

#pragma mark iOS System Completion Handlers

#if TARGET_OS_IPHONE
static NSMutableDictionary *gSystemCompletionHandlers = nil;

- (nullable GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler {
  return [[self class] systemCompletionHandlerForSessionIdentifier:_sessionIdentifier];
}

- (void)setSystemCompletionHandler:
    (nullable GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler {
  [[self class] setSystemCompletionHandler:systemCompletionHandler
                      forSessionIdentifier:_sessionIdentifier];
}

+ (void)setSystemCompletionHandler:
            (nullable GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler
              forSessionIdentifier:(NSString *)sessionIdentifier {
  if (!sessionIdentifier) {
    NSLog(@"%s with nil identifier", __PRETTY_FUNCTION__);
    return;
  }

  @synchronized([GTMSessionFetcher class]) {
    if (gSystemCompletionHandlers == nil && systemCompletionHandler != nil) {
      gSystemCompletionHandlers = [[NSMutableDictionary alloc] init];
    }
    // Use setValue: to remove the object if completionHandler is nil.
    [gSystemCompletionHandlers setValue:systemCompletionHandler forKey:sessionIdentifier];
  }
}

+ (nullable GTMSessionFetcherSystemCompletionHandler)systemCompletionHandlerForSessionIdentifier:
    (NSString *)sessionIdentifier {
  if (!sessionIdentifier) {
    return nil;
  }
  @synchronized([GTMSessionFetcher class]) {
    return [gSystemCompletionHandlers objectForKey:sessionIdentifier];
  }
}
#endif  // TARGET_OS_IPHONE

#pragma mark Getters and Setters

// clang-format off
// Don't re-format the @synthesize blocks:
@synthesize downloadResumeData = _downloadResumeData,
            configuration = _configuration,
            configurationBlock = _configurationBlock,
            sessionTask = _sessionTask,
            wasCreatedFromBackgroundSession = _wasCreatedFromBackgroundSession,
            clientWillReconnectBackgroundSession = _clientWillReconnectBackgroundSession,
            sessionUserInfo = _sessionUserInfo,
            taskDescription = _taskDescription,
            taskPriority = _taskPriority,
            usingBackgroundSession = _usingBackgroundSession,
            canShareSession = _canShareSession,
            completionHandler = _completionHandler,
            credential = _credential,
            proxyCredential = _proxyCredential,
            bodyData = _bodyData,
            bodyLength = _bodyLength,
            service = _service,
            serviceHost = _serviceHost,
            accumulateDataBlock = _accumulateDataBlock,
            receivedProgressBlock = _receivedProgressBlock,
            downloadProgressBlock = _downloadProgressBlock,
            resumeDataBlock = _resumeDataBlock,
            didReceiveResponseBlock = _didReceiveResponseBlock,
            challengeBlock = _challengeBlock,
            willRedirectBlock = _willRedirectBlock,
            sendProgressBlock = _sendProgressBlock,
            willCacheURLResponseBlock = _willCacheURLResponseBlock,
            retryBlock = _retryBlock,
            metricsCollectionBlock = _metricsCollectionBlock,
            retryFactor = _retryFactor,
            allowedInsecureSchemes = _allowedInsecureSchemes,
            allowLocalhostRequest = _allowLocalhostRequest,
            allowInvalidServerCertificates = _allowInvalidServerCertificates,
            cookieStorage = _cookieStorage,
            callbackQueue = _callbackQueue,
            initialBeginFetchDate = _initialBeginFetchDate,
            testBlock = _testBlock,
            testBlockAccumulateDataChunkCount = _testBlockAccumulateDataChunkCount,
            comment = _comment,
            log = _log,
            userAgentProvider = _userAgentProvider,
            stopFetchingTriggersCompletionHandler = _stopFetchingTriggersCompletionHandler;

#if !STRIP_GTM_FETCH_LOGGING
@synthesize redirectedFromURL = _redirectedFromURL,
            logRequestBody = _logRequestBody,
            logResponseBody = _logResponseBody,
            hasLoggedError = _hasLoggedError;
#endif

#if GTM_BACKGROUND_TASK_FETCHING
@synthesize backgroundTaskIdentifier = _backgroundTaskIdentifier,
            skipBackgroundTask = _skipBackgroundTask;
#endif
// clang-format on

- (nullable NSURLRequest *)request {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return [_request copy];
  }  // @synchronized(self)
}

- (void)setRequest:(nullable NSURLRequest *)request {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (![self isFetchingUnsynchronized]) {
      _request = [request mutableCopy];
    } else {
      GTMSESSION_ASSERT_DEBUG(0, @"request may not be set after beginFetch has been invoked");
    }
  }  // @synchronized(self)
}

- (nullable NSMutableURLRequest *)mutableRequestForTesting {
  // Allow tests only to modify the request, useful during retries.
  return _request;
}

// Internal method for updating the request property such as on redirects.
- (void)updateMutableRequest:(nullable NSMutableURLRequest *)request {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _request = request;
  }  // @synchronized(self)
}

// Set a header field value on the request. Header field value changes will not
// affect a fetch after the fetch has begun.
- (void)setRequestValue:(nullable NSString *)value forHTTPHeaderField:(NSString *)field {
  if (![self isFetching]) {
    [self updateRequestValue:value forHTTPHeaderField:field];
  } else {
    GTMSESSION_ASSERT_DEBUG(0, @"request may not be set after beginFetch has been invoked");
  }
}

// Internal method for updating request headers.
- (void)updateRequestValue:(nullable NSString *)value forHTTPHeaderField:(NSString *)field {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    [_request setValue:value forHTTPHeaderField:field];
  }  // @synchronized(self)
}

- (void)setResponse:(nullable NSURLResponse *)response {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _response = response;
  }  // @synchronized(self)
}

- (int64_t)bodyLength {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (_bodyLength == NSURLSessionTransferSizeUnknown) {
      if (_bodyData) {
        _bodyLength = (int64_t)_bodyData.length;
      } else if (_bodyFileURL) {
        NSNumber *fileSizeNum = nil;
        NSError *fileSizeError = nil;
        if ([_bodyFileURL getResourceValue:&fileSizeNum
                                    forKey:NSURLFileSizeKey
                                     error:&fileSizeError]) {
          _bodyLength = [fileSizeNum longLongValue];
        }
      }
    }
    return _bodyLength;
  }  // @synchronized(self)
}

- (BOOL)useUploadTask {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _useUploadTask;
  }  // @synchronized(self)
}

- (void)setUseUploadTask:(BOOL)flag {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (flag != _useUploadTask) {
      GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized],
                              @"useUploadTask should not change after beginFetch has been invoked");
      _useUploadTask = flag;
    }
  }  // @synchronized(self)
}

- (nullable NSURL *)bodyFileURL {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _bodyFileURL;
  }  // @synchronized(self)
}

- (void)setBodyFileURL:(nullable NSURL *)fileURL {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    // The comparison here is a trivial optimization and forgiveness for any client that
    // repeatedly sets the property, so it just uses pointer comparison rather than isEqual:.
    if (fileURL != _bodyFileURL) {
      GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized],
                              @"fileURL should not change after beginFetch has been invoked");

      _bodyFileURL = fileURL;
    }
  }  // @synchronized(self)
}

- (nullable GTMSessionFetcherBodyStreamProvider)bodyStreamProvider {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _bodyStreamProvider;
  }  // @synchronized(self)
}

- (void)setBodyStreamProvider:(nullable GTMSessionFetcherBodyStreamProvider)block {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized],
                            @"stream provider should not change after beginFetch has been invoked");

    _bodyStreamProvider = [block copy];
  }  // @synchronized(self)
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
- (nullable id<GTMFetcherAuthorizationProtocol>)authorizer {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _authorizer;
  }  // @synchronized(self)
}

- (void)setAuthorizer:(nullable id<GTMFetcherAuthorizationProtocol>)authorizer {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (authorizer != _authorizer) {
      if ([self isFetchingUnsynchronized]) {
        GTMSESSION_ASSERT_DEBUG(0,
                                @"authorizer should not change after beginFetch has been invoked");
      } else {
        _authorizer = authorizer;
      }
    }
  }  // @synchronized(self)
}
#pragma clang diagnostic pop

- (nullable NSData *)downloadedData {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _downloadedData;
  }  // @synchronized(self)
}

- (void)setDownloadedData:(nullable NSData *)data {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _downloadedData = [data mutableCopy];
  }  // @synchronized(self)
}

- (int64_t)downloadedLength {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _downloadedLength;
  }  // @synchronized(self)
}

- (void)setDownloadedLength:(int64_t)length {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _downloadedLength = length;
  }  // @synchronized(self)
}

- (nonnull dispatch_queue_t)callbackQueue {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _callbackQueue;
  }  // @synchronized(self)
}

- (void)setCallbackQueue:(nullable dispatch_queue_t)queue {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _callbackQueue = queue ?: dispatch_get_main_queue();
  }  // @synchronized(self)
}

- (nullable NSURLSession *)session {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _session;
  }  // @synchronized(self)
}

- (NSInteger)servicePriority {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _servicePriority;
  }  // @synchronized(self)
}

- (void)setServicePriority:(NSInteger)value {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (value != _servicePriority) {
      GTMSESSION_ASSERT_DEBUG(
          ![self isFetchingUnsynchronized],
          @"servicePriority should not change after beginFetch has been invoked");

      _servicePriority = value;
    }
  }  // @synchronized(self)
}

- (void)setSession:(nullable NSURLSession *)session {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _session = session;
  }  // @synchronized(self)
}

- (BOOL)canShareSession {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _canShareSession;
  }  // @synchronized(self)
}

- (void)setCanShareSession:(BOOL)flag {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _canShareSession = flag;
  }  // @synchronized(self)
}

- (BOOL)useBackgroundSession {
  // This reflects if the user requested a background session, not necessarily
  // if one was created. That is tracked with _usingBackgroundSession.
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _userRequestedBackgroundSession;
  }  // @synchronized(self)
}

- (void)setUseBackgroundSession:(BOOL)flag {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (flag != _userRequestedBackgroundSession) {
      GTMSESSION_ASSERT_DEBUG(
          ![self isFetchingUnsynchronized],
          @"useBackgroundSession should not change after beginFetch has been invoked");

      _userRequestedBackgroundSession = flag;
    }
  }  // @synchronized(self)
}

- (BOOL)isUsingBackgroundSession {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _usingBackgroundSession;
  }  // @synchronized(self)
}

- (void)setUsingBackgroundSession:(BOOL)flag {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _usingBackgroundSession = flag;
  }  // @synchronized(self)
}

- (BOOL)stopFetchingTriggersCompletionHandler {
  return _stopFetchingTriggersCompletionHandler;
}

- (void)setStopFetchingTriggersCompletionHandler:(BOOL)flag {
  if (_initialBeginFetchDate == nil) {
    _stopFetchingTriggersCompletionHandler = flag;
  } else {
    GTMSESSION_ASSERT_DEBUG(
        0, @"stopFetchingTriggersCompletionHandler should not change after fetcher starts");
  }
}

- (nullable NSURLSession *)sessionNeedingInvalidation {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _sessionNeedingInvalidation;
  }  // @synchronized(self)
}

- (void)setSessionNeedingInvalidation:(nullable NSURLSession *)session {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _sessionNeedingInvalidation = session;
  }  // @synchronized(self)
}

- (nonnull NSOperationQueue *)sessionDelegateQueue {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _delegateQueue;
  }  // @synchronized(self)
}

- (void)setSessionDelegateQueue:(nullable NSOperationQueue *)queue {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (queue != _delegateQueue) {
      if ([self isFetchingUnsynchronized]) {
        GTMSESSION_ASSERT_DEBUG(0, @"sessionDelegateQueue should not change after fetch begins");
      } else {
        _delegateQueue = queue ?: [NSOperationQueue mainQueue];
      }
    }
  }  // @synchronized(self)
}

- (BOOL)userStoppedFetching {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _userStoppedFetching;
  }  // @synchronized(self)
}

- (nullable id)userData {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _userData;
  }  // @synchronized(self)
}

- (void)setUserData:(nullable id)theObj {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _userData = theObj;
  }  // @synchronized(self)
}

- (nullable NSURL *)destinationFileURL {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _destinationFileURL;
  }  // @synchronized(self)
}

- (void)setDestinationFileURL:(nullable NSURL *)destinationFileURL {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (((_destinationFileURL == nil) && (destinationFileURL == nil)) ||
        [_destinationFileURL isEqual:destinationFileURL]) {
      return;
    }
    if (_sessionIdentifier) {
      // This is something we don't expect to happen in production.
      // However if it ever happen, leave a system log.
      NSLog(@"%@: Destination File URL changed from (%@) to (%@) after session identifier has "
            @"been created.",
            [self class], _destinationFileURL, destinationFileURL);
#if DEBUG
      // On both the simulator and devices, the path can change to the download file, but the name
      // shouldn't change. Technically, this isn't supported in the fetcher, but the change of
      // URL is expected to happen only across development runs through Xcode.
      __unused NSString *oldFilename = [_destinationFileURL lastPathComponent];
      __unused NSString *newFilename = [destinationFileURL lastPathComponent];
      GTMSESSION_ASSERT_DEBUG(
          [oldFilename isEqualToString:newFilename],
          @"Destination File URL cannot be changed after session identifier has been created");
#endif
    }
    _destinationFileURL = destinationFileURL;
  }  // @synchronized(self)
}

- (void)setProperties:(nullable NSDictionary *)dict {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _properties = [dict mutableCopy];
  }  // @synchronized(self)
}

- (nullable NSDictionary *)properties {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _properties;
  }  // @synchronized(self)
}

- (void)setProperty:(nullable id)obj forKey:(NSString *)key {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (_properties == nil && obj != nil) {
      _properties = [[NSMutableDictionary alloc] init];
    }
    [_properties setValue:obj forKey:key];
  }  // @synchronized(self)
}

- (nullable id)propertyForKey:(NSString *)key {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return [_properties objectForKey:key];
  }  // @synchronized(self)
}

- (void)addPropertiesFromDictionary:(NSDictionary *)dict {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (_properties == nil && dict != nil) {
      [self setProperties:[dict mutableCopy]];
    } else {
      [_properties addEntriesFromDictionary:dict];
    }
  }  // @synchronized(self)
}

- (void)setCommentWithFormat:(id)format, ... {
#if !STRIP_GTM_FETCH_LOGGING
  NSString *result = format;
  if (format) {
    va_list argList;
    va_start(argList, format);

    result = [[NSString alloc] initWithFormat:format arguments:argList];
    va_end(argList);
  }
  [self setComment:result];
#endif
}

#if !STRIP_GTM_FETCH_LOGGING
- (NSData *)loggedStreamData {
  return _loggedStreamData;
}

- (void)appendLoggedStreamData:dataToAdd {
  if (!_loggedStreamData) {
    _loggedStreamData = [NSMutableData data];
  }
  [_loggedStreamData appendData:dataToAdd];
}

- (void)clearLoggedStreamData {
  _loggedStreamData = nil;
}

- (void)setDeferResponseBodyLogging:(BOOL)deferResponseBodyLogging {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (deferResponseBodyLogging != _deferResponseBodyLogging) {
      _deferResponseBodyLogging = deferResponseBodyLogging;
      if (!deferResponseBodyLogging && !self.hasLoggedError) {
        [_delegateQueue addOperationWithBlock:^{
          [self logNowWithError:nil];
        }];
      }
    }
  }  // @synchronized(self)
}

- (BOOL)deferResponseBodyLogging {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _deferResponseBodyLogging;
  }  // @synchronized(self)
}

#else
+ (void)setLoggingEnabled:(BOOL)flag {
}

+ (BOOL)isLoggingEnabled {
  return NO;
}
#endif  // STRIP_GTM_FETCH_LOGGING

@end

@implementation GTMSessionCookieStorage {
  NSMutableArray *_cookies;
  NSHTTPCookieAcceptPolicy _policy;
}

- (id)init {
  self = [super init];
  if (self != nil) {
    _cookies = [[NSMutableArray alloc] init];
  }
  return self;
}

- (nullable NSArray *)cookies {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return [_cookies copy];
  }  // @synchronized(self)
}

- (void)setCookie:(NSHTTPCookie *)cookie {
  if (!cookie) return;
  if (_policy == NSHTTPCookieAcceptPolicyNever) return;

  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    [self internalSetCookie:cookie];
  }  // @synchronized(self)
}

// Note: this should only be called from inside a @synchronized(self) block.
- (void)internalSetCookie:(NSHTTPCookie *)newCookie {
  GTMSessionCheckSynchronized(self);

  if (_policy == NSHTTPCookieAcceptPolicyNever) return;

  BOOL isValidCookie =
      (newCookie.name.length > 0 && newCookie.domain.length > 0 && newCookie.path.length > 0);
  GTMSESSION_ASSERT_DEBUG(isValidCookie, @"invalid cookie: %@", newCookie);

  if (isValidCookie) {
    // Remove the cookie if it's currently in the array.
    NSHTTPCookie *oldCookie = [self cookieMatchingCookie:newCookie];
    if (oldCookie) {
      [_cookies removeObjectIdenticalTo:oldCookie];
    }

    if (![[self class] hasCookieExpired:newCookie]) {
      [_cookies addObject:newCookie];
    }
  }
}

// Add all cookies in the new cookie array to the storage,
// replacing stored cookies as appropriate.
//
// Side effect: removes expired cookies from the storage array.
- (void)setCookies:(nullable NSArray *)newCookies {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    [self removeExpiredCookies];

    for (NSHTTPCookie *newCookie in newCookies) {
      [self internalSetCookie:newCookie];
    }
  }  // @synchronized(self)
}

- (void)setCookies:(NSArray *)cookies
             forURL:(nullable NSURL *)URL
    mainDocumentURL:(nullable NSURL *)mainDocumentURL {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    if (_policy == NSHTTPCookieAcceptPolicyNever) {
      return;
    }

    if (_policy == NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain) {
      NSString *mainHost = mainDocumentURL.host;
      NSString *associatedHost = URL.host;
      if (!mainHost || ![associatedHost hasSuffix:mainHost]) {
        return;
      }
    }
  }  // @synchronized(self)
  [self setCookies:cookies];
}

- (void)deleteCookie:(NSHTTPCookie *)cookie {
  if (!cookie) return;

  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    NSHTTPCookie *foundCookie = [self cookieMatchingCookie:cookie];
    if (foundCookie) {
      [_cookies removeObjectIdenticalTo:foundCookie];
    }
  }  // @synchronized(self)
}

// Retrieve all cookies appropriate for the given URL, considering
// domain, path, cookie name, expiration, security setting.
// Side effect: removed expired cookies from the storage array.
- (nullable NSArray *)cookiesForURL:(NSURL *)theURL {
  NSMutableArray *foundCookies = nil;

  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    [self removeExpiredCookies];

    // We'll prepend "." to the desired domain, since we want the
    // actual domain "nytimes.com" to still match the cookie domain
    // ".nytimes.com" when we check it below with hasSuffix.
    NSString *host = theURL.host.lowercaseString;
    NSString *path = theURL.path;
    NSString *scheme = [theURL scheme];

    NSString *requestingDomain = nil;
    BOOL isLocalhostRetrieval = NO;

    if (IsLocalhost(host)) {
      isLocalhostRetrieval = YES;
    } else {
      if (host.length > 0) {
        requestingDomain = [@"." stringByAppendingString:host];
      }
    }

    for (NSHTTPCookie *storedCookie in _cookies) {
      NSString *cookieDomain = storedCookie.domain.lowercaseString;
      NSString *cookiePath = storedCookie.path;
      BOOL cookieIsSecure = [storedCookie isSecure];

      BOOL isDomainOK;

      if (isLocalhostRetrieval) {
        // Prior to 10.5.6, the domain stored into NSHTTPCookies for localhost
        // is "localhost.local"
        isDomainOK = (IsLocalhost(cookieDomain) || [cookieDomain isEqual:@"localhost.local"]);
      } else {
        // Ensure we're matching exact domain names. We prepended a dot to the
        // requesting domain, so we can also prepend one here if needed before
        // checking if the request contains the cookie domain.
        if (![cookieDomain hasPrefix:@"."]) {
          cookieDomain = [@"." stringByAppendingString:cookieDomain];
        }
        isDomainOK = [requestingDomain hasSuffix:cookieDomain];
      }

      BOOL isPathOK = [cookiePath isEqual:@"/"] || [path hasPrefix:cookiePath];
      BOOL isSecureOK =
          (!cookieIsSecure || [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame);

      if (isDomainOK && isPathOK && isSecureOK) {
        if (foundCookies == nil) {
          foundCookies = [NSMutableArray array];
        }
        [foundCookies addObject:storedCookie];
      }
    }
  }  // @synchronized(self)
  return foundCookies;
}

// Override methods from the NSHTTPCookieStorage (NSURLSessionTaskAdditions) category.
- (void)storeCookies:(NSArray *)cookies forTask:(NSURLSessionTask *)task {
  NSURLRequest *currentRequest = task.currentRequest;
  [self setCookies:cookies forURL:currentRequest.URL mainDocumentURL:nil];
}

- (void)getCookiesForTask:(NSURLSessionTask *)task
        completionHandler:(void (^)(NSArray<NSHTTPCookie *> *))completionHandler {
  if (completionHandler) {
    NSURLRequest *currentRequest = task.currentRequest;
    NSURL *currentRequestURL = currentRequest.URL;
    NSArray *cookies = [self cookiesForURL:currentRequestURL];
    completionHandler(cookies);
  }
}

// Return a cookie from the array with the same name, domain, and path as the
// given cookie, or else return nil if none found.
//
// Both the cookie being tested and all cookies in the storage array should
// be valid (non-nil name, domains, paths).
//
// Note: this should only be called from inside a @synchronized(self) block
- (nullable NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie {
  GTMSessionCheckSynchronized(self);

  NSString *name = cookie.name;
  NSString *domain = cookie.domain;
  NSString *path = cookie.path;

  GTMSESSION_ASSERT_DEBUG(name && domain && path,
                          @"Invalid stored cookie (name:%@ domain:%@ path:%@)", name, domain, path);

  for (NSHTTPCookie *storedCookie in _cookies) {
    if ([storedCookie.name isEqual:name] && [storedCookie.domain isEqual:domain] &&
        [storedCookie.path isEqual:path]) {
      return storedCookie;
    }
  }
  return nil;
}

// Internal routine to remove any expired cookies from the array, excluding
// cookies with nil expirations.
//
// Note: this should only be called from inside a @synchronized(self) block
- (void)removeExpiredCookies {
  GTMSessionCheckSynchronized(self);

  // Count backwards since we're deleting items from the array
  for (NSInteger idx = (NSInteger)_cookies.count - 1; idx >= 0; idx--) {
    NSHTTPCookie *storedCookie = [_cookies objectAtIndex:(NSUInteger)idx];
    if ([[self class] hasCookieExpired:storedCookie]) {
      [_cookies removeObjectAtIndex:(NSUInteger)idx];
    }
  }
}

+ (BOOL)hasCookieExpired:(NSHTTPCookie *)cookie {
  NSDate *expiresDate = [cookie expiresDate];
  if (expiresDate == nil) {
    // Cookies seem to have a Expires property even when the expiresDate method returns nil.
    id expiresVal = [[cookie properties] objectForKey:NSHTTPCookieExpires];
    if ([expiresVal isKindOfClass:[NSDate class]]) {
      expiresDate = expiresVal;
    }
  }
  BOOL hasExpired = (expiresDate != nil && [expiresDate timeIntervalSinceNow] < 0);
  return hasExpired;
}

- (void)removeAllCookies {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    [_cookies removeAllObjects];
  }  // @synchronized(self)
}

- (NSHTTPCookieAcceptPolicy)cookieAcceptPolicy {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    return _policy;
  }  // @synchronized(self)
}

- (void)setCookieAcceptPolicy:(NSHTTPCookieAcceptPolicy)cookieAcceptPolicy {
  @synchronized(self) {
    GTMSessionMonitorSynchronized(self);

    _policy = cookieAcceptPolicy;
  }  // @synchronized(self)
}

@end

void GTMSessionFetcherAssertValidSelector(id _Nullable obj, SEL _Nullable sel, ...) {
  // Verify that the object's selector is implemented with the proper
  // number and type of arguments
#if DEBUG
  va_list argList;
  va_start(argList, sel);

  if (obj && sel) {
    // Check that the selector is implemented
    if (![obj respondsToSelector:sel]) {
      NSLog(@"\"%@\" selector \"%@\" is unimplemented or misnamed",
            NSStringFromClass([(id)obj class]), NSStringFromSelector((SEL)sel));
      NSCAssert(0, @"callback selector unimplemented or misnamed");
    } else {
      const char *expectedArgType;
      unsigned int argCount = 2;  // skip self and _cmd
      NSMethodSignature *sig = [obj methodSignatureForSelector:sel];

      // Check that each expected argument is present and of the correct type
      while ((expectedArgType = va_arg(argList, const char *)) != 0) {
        if ([sig numberOfArguments] > argCount) {
          const char *foundArgType = [sig getArgumentTypeAtIndex:argCount];

          if (0 != strncmp(foundArgType, expectedArgType, strlen(expectedArgType))) {
            NSLog(@"\"%@\" selector \"%@\" argument %d should be type %s",
                  NSStringFromClass([(id)obj class]), NSStringFromSelector((SEL)sel),
                  (argCount - 2), expectedArgType);
            NSCAssert(0, @"callback selector argument type mistake");
          }
        }
        argCount++;
      }

      // Check that the proper number of arguments are present in the selector
      if (argCount != [sig numberOfArguments]) {
        NSLog(@"\"%@\" selector \"%@\" should have %d arguments",
              NSStringFromClass([(id)obj class]), NSStringFromSelector((SEL)sel), (argCount - 2));
        NSCAssert(0, @"callback selector arguments incorrect");
      }
    }
  }

  va_end(argList);
#endif
}

NSString *GTMFetcherCleanedUserAgentString(NSString *str) {
  // Reference http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html
  // and http://www-archive.mozilla.org/build/user-agent-strings.html

  if (str == nil) return @"";

  NSMutableString *result = [NSMutableString stringWithString:str];

  // Replace spaces and commas with underscores
  [result replaceOccurrencesOfString:@" "
                          withString:@"_"
                             options:0
                               range:NSMakeRange(0, result.length)];
  [result replaceOccurrencesOfString:@","
                          withString:@"_"
                             options:0
                               range:NSMakeRange(0, result.length)];

  // Delete http token separators and remaining whitespace
  static NSCharacterSet *charsToDelete = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    // Make a set of unwanted characters
    NSString *const kSeparators = @"()<>@;:\\\"/[]?={}";

    NSMutableCharacterSet *mutableChars =
        [[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy];
    [mutableChars addCharactersInString:kSeparators];
    charsToDelete = [mutableChars copy];  // hang on to an immutable copy
  });

  while (1) {
    NSRange separatorRange = [result rangeOfCharacterFromSet:charsToDelete];
    if (separatorRange.location == NSNotFound) break;

    [result deleteCharactersInRange:separatorRange];
  };

  return result;
}

NSString *GTMFetcherSystemVersionString(void) {
  static NSString *sSavedSystemString;

  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
#if TARGET_OS_WATCH
    // watchOS - WKInterfaceDevice

    WKInterfaceDevice *currentDevice = [WKInterfaceDevice currentDevice];

    NSString *rawModel = [currentDevice model];
    NSString *model = GTMFetcherCleanedUserAgentString(rawModel);

    NSString *systemVersion = [currentDevice systemVersion];

#if TARGET_OS_SIMULATOR
    NSString *hardwareModel = @"sim";
#else
    NSString *hardwareModel;
    struct utsname unameRecord;
    if (uname(&unameRecord) == 0) {
      NSString *machineName = @(unameRecord.machine);
      hardwareModel = GTMFetcherCleanedUserAgentString(machineName);
    }
    if (hardwareModel.length == 0) {
      hardwareModel = @"unk";
    }
#endif

    sSavedSystemString =
        [[NSString alloc] initWithFormat:@"%@/%@ hw/%@", model, systemVersion, hardwareModel];
    // Example:  Apple_Watch/3.0 hw/Watch1_2
#elif TARGET_OS_TV || TARGET_OS_IOS
    // iOS and tvOS have UIDevice, use that.
    UIDevice *currentDevice = [UIDevice currentDevice];

    NSString *rawModel = [currentDevice model];
    NSString *model = GTMFetcherCleanedUserAgentString(rawModel);

    NSString *systemVersion = [currentDevice systemVersion];

#if TARGET_OS_SIMULATOR
    NSString *hardwareModel = @"sim";
#else
    NSString *hardwareModel;
    struct utsname unameRecord;
    if (uname(&unameRecord) == 0) {
      NSString *machineName = @(unameRecord.machine);
      hardwareModel = GTMFetcherCleanedUserAgentString(machineName);
    }
    if (hardwareModel.length == 0) {
      hardwareModel = @"unk";
    }
#endif

    sSavedSystemString = [[NSString alloc] initWithFormat:@"%@/%@ hw/%@",
                          model, systemVersion, hardwareModel];
    // Example:  iPod_Touch/2.2 hw/iPod1_1
    // Example:  Apple_TV/9.2 hw/AppleTV5,3
#elif TARGET_OS_OSX
    // Mac build
    NSProcessInfo *procInfo = [NSProcessInfo processInfo];
    NSString *versString;
    NSOperatingSystemVersion version = procInfo.operatingSystemVersion;
    versString = [NSString stringWithFormat:@"%ld.%ld.%ld", (long)version.majorVersion,
                                            (long)version.minorVersion, (long)version.patchVersion];

    sSavedSystemString = [[NSString alloc] initWithFormat:@"MacOSX/%@", versString];
#elif defined(_SYS_UTSNAME_H)
    // Foundation-only build
    struct utsname unameRecord;
    uname(&unameRecord);

    sSavedSystemString = [NSString stringWithFormat:@"%s/%s",
                          unameRecord.sysname, unameRecord.release]; // "Darwin/8.11.1"
#else
#error No branch taken for a default user agent
#endif
  });
  return sSavedSystemString;
}

@implementation GTMUserAgentStringProvider

@synthesize userAgent = _userAgent;

- (instancetype)initWithUserAgentString:(NSString *)userAgentString {
  self = [super init];
  if (self) {
    _userAgent = [userAgentString copy];
  }
  return self;
}

#pragma mark - GTMUserAgentProvider

- (nullable NSString *)cachedUserAgent {
  return _userAgent;
}

- (NSString *)userAgent {
  return _userAgent;
}

@end

@interface GTMStandardUserAgentProvider () {
  NSBundle *_Nullable _bundle;
}

@property(atomic, copy) NSString *cachedUserAgent;

@end

@implementation GTMStandardUserAgentProvider

@synthesize cachedUserAgent = _cachedUserAgent;

- (instancetype)initWithBundle:(nullable NSBundle *)bundle {
  self = [super init];
  if (self) {
    _bundle = bundle;
  }
  return self;
}

#pragma mark - GTMUserAgentProvider

- (NSString *)userAgent {
  NSString *userAgent = self.cachedUserAgent;
  if (!userAgent) {
    // This might invoke `GTMFetcherStandardUserAgentString()` more than once if two threads enter
    // here concurrently, but the result will be the same for both.
    userAgent = GTMFetcherStandardUserAgentString(_bundle);
    self.cachedUserAgent = userAgent;
  }
  return userAgent;
}

@end

NSString *GTMFetcherStandardUserAgentString(NSBundle *_Nullable bundle) {
  NSString *result = [NSString stringWithFormat:@"%@ %@", GTMFetcherApplicationIdentifier(bundle),
                                                GTMFetcherSystemVersionString()];
  return result;
}

NSString *GTMFetcherApplicationIdentifier(NSBundle *_Nullable bundle) {
  @synchronized([GTMSessionFetcher class]) {
    static NSMutableDictionary *sAppIDMap = nil;

    // If there's a bundle ID, use that; otherwise, use the process name
    if (bundle == nil) {
      bundle = [NSBundle mainBundle];
    }
    NSString *bundleID = [bundle bundleIdentifier];
    if (bundleID == nil) {
      bundleID = @"";
    }

    NSString *identifier = [sAppIDMap objectForKey:bundleID];
    if (identifier) return identifier;

    // Apps may add a string to the info.plist to uniquely identify different builds.
    identifier = [bundle objectForInfoDictionaryKey:@"GTMUserAgentID"];
    if (identifier.length == 0) {
      if (bundleID.length > 0) {
        identifier = bundleID;
      } else {
        // Fall back on the procname, prefixed by "proc" to flag that it's
        // autogenerated and perhaps unreliable
        NSString *procName = [[NSProcessInfo processInfo] processName];
        identifier = [NSString stringWithFormat:@"proc_%@", procName];
      }
    }

    // Clean up whitespace and special characters
    identifier = GTMFetcherCleanedUserAgentString(identifier);

    // If there's a version number, append that
    NSString *version = [bundle objectForInfoDictionaryKey:@"GTMUserAgentVersion"];
    if (version.length == 0) {
      version = [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
      if (version.length == 0) {
        version = [bundle objectForInfoDictionaryKey:@"CFBundleVersion"];
      }
    }

    // Clean up whitespace and special characters
    version = GTMFetcherCleanedUserAgentString(version);

    // Glue the two together (cleanup done above or else cleanup would strip the
    // slash)
    if (version.length > 0) {
      identifier = [identifier stringByAppendingFormat:@"/%@", version];
    }

    if (sAppIDMap == nil) {
      sAppIDMap = [[NSMutableDictionary alloc] init];
    }
    [sAppIDMap setObject:identifier forKey:bundleID];
    return identifier;
  }
}

#if DEBUG && (!defined(NS_BLOCK_ASSERTIONS) || GTMSESSION_ASSERT_AS_LOG)
@implementation GTMSessionSyncMonitorInternal {
  NSValue *_objectKey;        // The synchronize target object.
  const char *_functionName;  // The function containing the monitored sync block.
}

- (instancetype)initWithSynchronizationObject:(id)object
                               allowRecursive:(BOOL)allowRecursive
                                 functionName:(const char *)functionName {
  self = [super init];
  if (self) {
    Class threadKey = [GTMSessionSyncMonitorInternal class];
    _objectKey = [NSValue valueWithNonretainedObject:object];
    _functionName = functionName;

    NSMutableDictionary *threadDict = [NSThread currentThread].threadDictionary;
    NSMutableDictionary *counters = threadDict[threadKey];
    if (counters == nil) {
      counters = [NSMutableDictionary dictionary];
      threadDict[(id)threadKey] = counters;
    }
    NSCountedSet *functionNamesCounter = counters[_objectKey];
    NSUInteger numberOfSyncingFunctions = functionNamesCounter.count;

    if (!allowRecursive) {
      BOOL isTopLevelSyncScope = (numberOfSyncingFunctions == 0);
      NSArray *stack = [NSThread callStackSymbols];
      GTMSESSION_ASSERT_DEBUG(isTopLevelSyncScope,
                              @"*** Recursive sync on %@ at %s; previous sync at %@\n%@",
                              [object class], functionName, functionNamesCounter.allObjects,
                              [stack subarrayWithRange:NSMakeRange(1, stack.count - 1)]);
    }

    if (!functionNamesCounter) {
      functionNamesCounter = [NSCountedSet set];
      counters[_objectKey] = functionNamesCounter;
    }
    [functionNamesCounter addObject:(id _Nonnull) @(functionName)];
  }
  return self;
}

- (void)dealloc {
  Class threadKey = [GTMSessionSyncMonitorInternal class];

  NSMutableDictionary *threadDict = [NSThread currentThread].threadDictionary;
  NSMutableDictionary *counters = threadDict[threadKey];
  NSCountedSet *functionNamesCounter = counters[_objectKey];
  NSString *functionNameStr = @(_functionName);
  NSUInteger numberOfSyncsByThisFunction = [functionNamesCounter countForObject:functionNameStr];
  NSArray *stack = [NSThread callStackSymbols];
  GTMSESSION_ASSERT_DEBUG(numberOfSyncsByThisFunction > 0, @"Sync not found on %@ at %s\n%@",
                          [_objectKey.nonretainedObjectValue class], _functionName,
                          [stack subarrayWithRange:NSMakeRange(1, stack.count - 1)]);
  [functionNamesCounter removeObject:functionNameStr];
  if (functionNamesCounter.count == 0) {
    [counters removeObjectForKey:_objectKey];
  }
}

+ (nullable NSArray *)functionsHoldingSynchronizationOnObject:(id)object {
  Class threadKey = [GTMSessionSyncMonitorInternal class];
  NSValue *localObjectKey = [NSValue valueWithNonretainedObject:object];

  NSMutableDictionary *threadDict = [NSThread currentThread].threadDictionary;
  NSMutableDictionary *counters = threadDict[threadKey];
  NSCountedSet *functionNamesCounter = counters[localObjectKey];
  return functionNamesCounter.count > 0 ? functionNamesCounter.allObjects : nil;
}
@end
#endif  // DEBUG && (!defined(NS_BLOCK_ASSERTIONS) || GTMSESSION_ASSERT_AS_LOG)
NS_ASSUME_NONNULL_END
