TNLExample/TAPI/TAPIResponse.m (130 lines of code) (raw):
//
// TAPIResponse.m
// TwitterNetworkLayer
//
// Created on 10/17/14.
// Copyright © 2020 Twitter. All rights reserved.
//
#import "TAPIError.h"
#import "TAPIResponse.h"
#import "TNL_Project.h"
NS_INLINE BOOL _DataBeginsWithHTMLDocType(NSData *data)
{
static const char sDocType[] = "<!DOCTYPE html";
static const size_t sDocTypeLength = (sizeof(sDocType) / sizeof(sDocType[0])) - 1; // minus 1 to ignore the NULL terminator
return data.length >= sDocTypeLength && 0 == strncmp(data.bytes, sDocType, sDocTypeLength);
}
static id _ParseAPIResponse(TNLResponseInfo *info, NSError ** parseErrorOut, NSError ** apiErrorOut);
static NSArray *_ExtractAPIErrors(id parsedObject);
@implementation TAPIResponse
@synthesize apiError = _apiError;
@synthesize parseError = _parseError;
@synthesize parsedObject = _parsedObject;
- (void)prepare
{
[super prepare];
if (!_operationError) {
NSError *apiError;
NSError *parseError;
_parsedObject = _ParseAPIResponse(_info, &parseError, &apiError);
_parseError = parseError;
_apiError = apiError;
TNLAttemptMetrics *metrics = _metrics.attemptMetrics.lastObject;
metrics.responseBodyParseError = parseError;
if (apiError) {
metrics.APIErrors = @[apiError];
}
}
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
_parsedObject = [coder decodeObjectOfClasses:[NSSet setWithObjects:[NSString class], [NSNumber class], [NSArray class], [NSDictionary class], nil]
forKey:@"parsedObject"];
_parseError = [coder decodeObjectOfClass:[NSError class] forKey:@"parseError"];
_apiError = [coder decodeObjectOfClass:[NSError class] forKey:@"apiError"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[super encodeWithCoder:aCoder];
[aCoder encodeObject:TNLErrorToSecureCodingError(_parsedObject) forKey:@"parsedObject"];
[aCoder encodeObject:TNLErrorToSecureCodingError(_apiError) forKey:@"apiError"];
[aCoder encodeObject:TNLErrorToSecureCodingError(_parseError) forKey:@"parseError"];
}
- (NSError *)anyError
{
return self.operationError ?: self.parseError ?: self.apiError;
}
@end
static id _ParseAPIResponse(TNLResponseInfo *info, NSError ** errorOut, NSError ** apiErrorOut)
{
id json = nil;
NSError *parseError = nil;
__block NSError *apiError = TNLHTTPStatusCodeIsSuccess(info.statusCode) ? nil : [NSError errorWithDomain:TAPIErrorDomain code:0 userInfo:nil];
NSInteger statusCode = info.statusCode;
NSData *data = info.data;
TNLAssert(statusCode > 0);
BOOL hasDocTypePrefix = _DataBeginsWithHTMLDocType(data);
if (hasDocTypePrefix) {
parseError = [NSError errorWithDomain:TAPIOperationErrorDomain
code:TAPIOperationErrorCodeServiceEncounteredTechnicalError
userInfo:nil];
} else {
json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError];
if (json) {
NSArray *apiErrors = _ExtractAPIErrors(json);
// Underlying behavior in some 4XX errors
if (TNLHTTPStatusCodeIsClientError(statusCode)) {
[apiErrors enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSError *currentError = obj;
if ([currentError.domain isEqualToString:TAPIErrorDomain]) {
apiError = currentError;
*stop = YES;
}
}];
}
} else {
parseError = [NSError errorWithDomain:TAPIParseErrorDomain
code:TAPIParseErrorCodeCannotParseResponse
userInfo:(parseError) ? @{ NSUnderlyingErrorKey : parseError } : nil];
}
}
if (errorOut) {
*errorOut = parseError;
}
if (apiErrorOut) {
*apiErrorOut = apiError;
}
return json;
}
static NSArray *_ExtractAPIErrors(id parsedObject)
{
TNLAssert(parsedObject != nil);
NSMutableArray *errorItems = [[NSMutableArray alloc] init];
if ([parsedObject isKindOfClass:[NSDictionary class]]) {
id errors = [parsedObject objectForKey:@"errors"];
if ([errors isKindOfClass:[NSArray class]]) {
[errors enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
BOOL successfullyParsedError = NO;
if ([obj isKindOfClass:[NSDictionary class]]) {
id codeObject = [obj objectForKey:@"code"];
id messageObject = [obj objectForKey:@"message"];
if (codeObject && messageObject) {
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
if (messageObject) {
userInfo[NSLocalizedDescriptionKey] = messageObject;
}
NSInteger code = [codeObject integerValue];
id timestamp = [obj objectForKey:@"timestamp"];
if (timestamp) {
userInfo[@"timestamp"] = timestamp;
}
[errorItems addObject:[NSError errorWithDomain:TAPIErrorDomain
code:code
userInfo:userInfo]];
successfullyParsedError = YES;
}
}
if (!successfullyParsedError) {
NSLog(@"Failed to parse server error:[%@]", obj);
}
}];
}
}
return errorItems;
}