diff --git a/SDWebImage/Core/SDWebImageDownloader.m b/SDWebImage/Core/SDWebImageDownloader.m index 2eeb05e4..cd6b7966 100644 --- a/SDWebImage/Core/SDWebImageDownloader.m +++ b/SDWebImage/Core/SDWebImageDownloader.m @@ -349,6 +349,14 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext; operation.minimumProgressInterval = MIN(MAX(self.config.minimumProgressInterval, 0), 1); } + if ([operation respondsToSelector:@selector(setAcceptableStatusCodes:)]) { + operation.acceptableStatusCodes = self.config.acceptableStatusCodes; + } + + if ([operation respondsToSelector:@selector(setAcceptableContentTypes:)]) { + operation.acceptableContentTypes = self.config.acceptableContentTypes; + } + if (options & SDWebImageDownloaderHighPriority) { operation.queuePriority = NSOperationQueuePriorityHigh; } else if (options & SDWebImageDownloaderLowPriority) { diff --git a/SDWebImage/Core/SDWebImageDownloaderConfig.h b/SDWebImage/Core/SDWebImageDownloaderConfig.h index 5a8cf583..9d5e67bf 100644 --- a/SDWebImage/Core/SDWebImageDownloaderConfig.h +++ b/SDWebImage/Core/SDWebImageDownloaderConfig.h @@ -95,4 +95,19 @@ typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) { */ @property (nonatomic, copy, nullable) NSString *password; +/** + * Set the acceptable HTTP Response status code. The status code which beyond the range will mark the download operation failed. + * For example, if we config [200, 400) but server response is 503, the download will fail with error code `SDWebImageErrorInvalidDownloadStatusCode`. + * Defaults to [200,400). Nil means no validation at all. + */ +@property (nonatomic, copy, nullable) NSIndexSet *acceptableStatusCodes; + +/** + * Set the acceptable HTTP Response content type. The content type beyond the set will mark the download operation failed. + * For example, if we config ["image/png"] but server response is "application/json", the download will fail with error code `SDWebImageErrorInvalidDownloadContentType`. + * Normally you don't need this for image format detection because we use image's data file signature magic bytes: https://en.wikipedia.org/wiki/List_of_file_signatures + * Defaults to nil. Nil means no validation at all. + */ +@property (nonatomic, copy, nullable) NSSet *acceptableContentTypes; + @end diff --git a/SDWebImage/Core/SDWebImageDownloaderConfig.m b/SDWebImage/Core/SDWebImageDownloaderConfig.m index 1fc93308..6120bd8a 100644 --- a/SDWebImage/Core/SDWebImageDownloaderConfig.m +++ b/SDWebImage/Core/SDWebImageDownloaderConfig.m @@ -26,6 +26,7 @@ static SDWebImageDownloaderConfig * _defaultDownloaderConfig; _maxConcurrentDownloads = 6; _downloadTimeout = 15.0; _executionOrder = SDWebImageDownloaderFIFOExecutionOrder; + _acceptableStatusCodes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 100)]; } return self; } @@ -41,6 +42,8 @@ static SDWebImageDownloaderConfig * _defaultDownloaderConfig; config.urlCredential = self.urlCredential; config.username = self.username; config.password = self.password; + config.acceptableStatusCodes = self.acceptableStatusCodes; + config.acceptableContentTypes = self.acceptableContentTypes; return config; } diff --git a/SDWebImage/Core/SDWebImageDownloaderOperation.h b/SDWebImage/Core/SDWebImageDownloaderOperation.h index 5fc19289..1cd2a50d 100644 --- a/SDWebImage/Core/SDWebImageDownloaderOperation.h +++ b/SDWebImage/Core/SDWebImageDownloaderOperation.h @@ -37,8 +37,12 @@ @optional @property (strong, nonatomic, readonly, nullable) NSURLSessionTask *dataTask; @property (strong, nonatomic, readonly, nullable) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); + +// These operation-level config was inherited from downloader. See `SDWebImageDownloaderConfig` for documentation. @property (strong, nonatomic, nullable) NSURLCredential *credential; @property (assign, nonatomic) double minimumProgressInterval; +@property (copy, nonatomic, nullable) NSIndexSet *acceptableStatusCodes; +@property (copy, nonatomic, nullable) NSSet *acceptableContentTypes; @end @@ -85,6 +89,21 @@ */ @property (assign, nonatomic) double minimumProgressInterval; +/** + * Set the acceptable HTTP Response status code. The status code which beyond the range will mark the download operation failed. + * For example, if we config [200, 400) but server response is 503, the download will fail with error code `SDWebImageErrorInvalidDownloadStatusCode`. + * Defaults to [200,400). Nil means no validation at all. + */ +@property (copy, nonatomic, nullable) NSIndexSet *acceptableStatusCodes; + +/** + * Set the acceptable HTTP Response content type. The content type beyond the set will mark the download operation failed. + * For example, if we config ["image/png"] but server response is "application/json", the download will fail with error code `SDWebImageErrorInvalidDownloadContentType`. + * Normally you don't need this for image format detection because we use image's data file signature magic bytes: https://en.wikipedia.org/wiki/List_of_file_signatures + * Defaults to nil. Nil means no validation at all. + */ +@property (copy, nonatomic, nullable) NSSet *acceptableContentTypes; + /** * The options for the receiver. */ diff --git a/SDWebImage/Core/SDWebImageDownloaderOperation.m b/SDWebImage/Core/SDWebImageDownloaderOperation.m index 1de1e7e9..34a98adc 100644 --- a/SDWebImage/Core/SDWebImageDownloaderOperation.m +++ b/SDWebImage/Core/SDWebImageDownloaderOperation.m @@ -332,18 +332,37 @@ didReceiveResponse:(NSURLResponse *)response self.expectedSize = expected; self.response = response; - NSInteger statusCode = [response respondsToSelector:@selector(statusCode)] ? ((NSHTTPURLResponse *)response).statusCode : 200; - // Status code should between [200,400) - BOOL statusCodeValid = statusCode >= 200 && statusCode < 400; + // Check status code valid (defaults [200,400)) + NSInteger statusCode = [response isKindOfClass:NSHTTPURLResponse.class] ? ((NSHTTPURLResponse *)response).statusCode : 0; + BOOL statusCodeValid = YES; + if (valid && statusCode > 0 && self.acceptableStatusCodes) { + statusCodeValid = [self.acceptableStatusCodes containsIndex:statusCode]; + } if (!statusCodeValid) { valid = NO; - self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadStatusCode userInfo:@{NSLocalizedDescriptionKey : @"Download marked as failed because response status code is not in 200-400", SDWebImageErrorDownloadStatusCodeKey : @(statusCode)}]; + self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain + code:SDWebImageErrorInvalidDownloadStatusCode + userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Download marked as failed because of invalid response status code %ld", (long)statusCode], SDWebImageErrorDownloadStatusCodeKey: @(statusCode)}]; + } + // Check content type valid (defaults nil) + NSString *contentType = [response isKindOfClass:NSHTTPURLResponse.class] ? ((NSHTTPURLResponse *)response).MIMEType : nil; + BOOL contentTypeValid = YES; + if (valid && contentType.length > 0 && self.acceptableContentTypes) { + contentTypeValid = [self.acceptableContentTypes containsObject:contentType]; + } + if (!contentTypeValid) { + valid = NO; + self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain + code:SDWebImageErrorInvalidDownloadContentType + userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Download marked as failed because of invalid response content type %@", contentType], SDWebImageErrorDownloadContentTypeKey: contentType}]; } //'304 Not Modified' is an exceptional one //URLSession current behavior will return 200 status code when the server respond 304 and URLCache hit. But this is not a standard behavior and we just add a check - if (statusCode == 304 && !self.cachedData) { + if (valid && statusCode == 304 && !self.cachedData) { valid = NO; - self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCacheNotModified userInfo:@{NSLocalizedDescriptionKey : @"Download response status code is 304 not modified and ignored"}]; + self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain + code:SDWebImageErrorCacheNotModified + userInfo:@{NSLocalizedDescriptionKey: @"Download response status code is 304 not modified and ignored"}]; } if (valid) { diff --git a/SDWebImage/Core/SDWebImageError.h b/SDWebImage/Core/SDWebImageError.h index 2b412010..acae0801 100644 --- a/SDWebImage/Core/SDWebImageError.h +++ b/SDWebImage/Core/SDWebImageError.h @@ -13,6 +13,8 @@ FOUNDATION_EXPORT NSErrorDomain const _Nonnull SDWebImageErrorDomain; /// The HTTP status code for invalid download response (NSNumber *) FOUNDATION_EXPORT NSErrorUserInfoKey const _Nonnull SDWebImageErrorDownloadStatusCodeKey; +/// The HTTP MIME content type for invalid download response (NSString *) +FOUNDATION_EXPORT NSErrorUserInfoKey const _Nonnull SDWebImageErrorDownloadContentTypeKey; /// SDWebImage error domain and codes typedef NS_ERROR_ENUM(SDWebImageErrorDomain, SDWebImageError) { @@ -24,4 +26,5 @@ typedef NS_ERROR_ENUM(SDWebImageErrorDomain, SDWebImageError) { SDWebImageErrorInvalidDownloadStatusCode = 2001, // The image download response a invalid status code. You can check the status code in error's userInfo under `SDWebImageErrorDownloadStatusCodeKey` SDWebImageErrorCancelled = 2002, // The image loading operation is cancelled before finished, during either async disk cache query, or waiting before actual network request. For actual network request error, check `NSURLErrorDomain` error domain and code. SDWebImageErrorInvalidDownloadResponse = 2003, // When using response modifier, the modified download response is nil and marked as failed. + SDWebImageErrorInvalidDownloadContentType = 2004, // The image download response a invalid content type. You can check the MIME content type in error's userInfo under `SDWebImageErrorDownloadContentTypeKey` }; diff --git a/SDWebImage/Core/SDWebImageError.m b/SDWebImage/Core/SDWebImageError.m index 6d174769..29a7039b 100644 --- a/SDWebImage/Core/SDWebImageError.m +++ b/SDWebImage/Core/SDWebImageError.m @@ -11,3 +11,4 @@ NSErrorDomain const _Nonnull SDWebImageErrorDomain = @"SDWebImageErrorDomain"; NSErrorUserInfoKey const _Nonnull SDWebImageErrorDownloadStatusCodeKey = @"SDWebImageErrorDownloadStatusCodeKey"; +NSErrorUserInfoKey const _Nonnull SDWebImageErrorDownloadContentTypeKey = @"SDWebImageErrorDownloadContentTypeKey";