解决 Unacceptable Content-Type 最近在通过 API 的形式访问腾讯云的 COS 服务 时,一直请求失败。通过 po error
命令打印出 AFNetworking 回调方法中的 NSError 对象,控制台输出如下:
1 2 3 4 5 6 7 8 9 10 11 (lldb)po error Error Domain=com.alamofire.error.serialization.response Code=-1016 "Request failed: unacceptable content-type: application/x-www-form-urlencoded" UserInfo={NSLocalizedDescription=Request failed: unacceptable content-type: application/x-www-form-urlencoded, NSErrorFailingURLKey=https://my.url, com.alamofire.serialization.response.error.data=<mydata>, com.alamofire.serialization.response.error.response=<NSHTTPURLResponse: 0x608000037600> { URL: https://my.url } { Status Code: 200, Headers { //...... "Content-Type" = ( "application/x-www-form-urlencoded" ); Server = ( "tencent-cos" ); //...... } }}
比较奇怪的一点是,可以看到腾讯云返回的 Status Code 是 200,而且如果查看 error 的 userinfo 信息 error.userInfo[@"com.alamofire.serialization.response.error.data"]
,是可以看到返回的 HTTP Body 信息的。这说明我们的请求是成功了的,毕竟正确的数据已经返回了,只是 AFNetworking 认为失败了。
错误原因 根据报错信息,可以看到错误的原因是 unacceptable content-type: application/x-www-form-urlencoded
。也就是腾讯云返回给我们的 content type 并不能被 AFNetworking 解析。而事实上这个接口中,返回的 body 信息本身就是我们需要的二进制数据,并不需要解析。因此要解决这个问题,只需要让 AFNetworking 不认为这是个错误就可以了,思路就是让它认为 application/x-www-form-urlencoded
是可以接受的。
添加 Content-Type 最直接的想法,就是我们取出 AFNetworking 支持的 content-type 集合,再把腾讯云的这个值添加进去:
1 2 3 NSMutableSet *set = [manager.responseSerializer.acceptableContentTypes mutableCopy];[set addObject:@"application/x-www-form-urlencoded" ]; manager.responseSerializer.acceptableContentTypes = [set copy ];
再次运行,果然不再报错说不接受 content-type 了,而是换了个新的错误:
1 Error Domain=NSCocoaErrorDomain Code=3840 "JSON text did not start with array or object and option to allow fragments not set." UserInfo={NSDebugDescription=JSON text did not start with array or object and option to allow fragments not set.}
新的错误信息提示我们 JSON 格式不正确。但是,腾讯云的这个接口并不会返回结构化的数据,body 里面只是二进制数据。而且,就算要返回被编码的信息,也是 XML 的,并不是 JSON。如果 AFNetworking 以 JSON 的格式去解析,当然会产生错误。
要解决这个问题,靠直觉就不够了,需要看看 AFNetworking 的源码。
寻找问题根源 我们跳转到 acceptableContentTypes
的定义中,在 AFURLResponseSerialization.m
文件中,可以看到这样一个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 - (BOOL )validateResponse:(NSHTTPURLResponse *)response data:(NSData *)data error:(NSError * __autoreleasing *)error { BOOL responseIsValid = YES ; NSError *validationError = nil ; if (response && [response isKindOfClass:[NSHTTPURLResponse class ]]) { if (self .acceptableContentTypes && ![self .acceptableContentTypes containsObject:[response MIMEType]] && !([response MIMEType] == nil && [data length] == 0 )) { responseIsValid = NO ; } } return responseIsValid; }
可以看到它确实有在判断接收到的 MIME type 是不是被包含在 acceptableContentTypes 里面的。由于我们刚才的添加,这里是可以被验证通过的,之前的思路肯定是正确的。就需要找到哪个地方产生了新问题。
查看 AFHTTPSessionManager
的实现,可以看到这两个初始化方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + (instancetype )manager { return [[[self class ] alloc] initWithBaseURL:nil ]; } - (instancetype )initWithBaseURL:(NSURL *)url sessionConfiguration:(NSURLSessionConfiguration *)configuration { self = [super initWithSessionConfiguration:configuration]; if (!self ) { return nil ; } self .requestSerializer = [AFHTTPRequestSerializer serializer]; self .responseSerializer = [AFJSONResponseSerializer serializer]; return self ; }
原来,在我们通过 AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
方法初始化 manager 的时候,它的 responseSerializer 就被设置成了 AFJSONResponseSerializer
。终于找到了问题的根源!
当然,这种错误是由于我们的接口返回的不是 JSON 数据导致的。如果你的接口返回的是 JSON,那么问题应该在上一步就已经解决了。
解决问题 我们只需要把 manager 的 responseSerializer 换掉就可以了:
1 manager.responseSerializer = [[AFHTTPResponseSerializer alloc] init];
使用 HTTPResponseSerializer,不需要它来解析 JSON。
再次运行程序,就可以成功拿到数据了。
附录 腾讯云的 API 在签名时需要做 md5 / SHA-1 / HMAC - SHA1 等加密算法。正确可用的实现不太好找,故将这几种算法代码附在这里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #import <CommonCrypto/CommonDigest.h> #import <CommonCrypto/CommonHMAC.h> - (NSString *)sha1WithStr :(NSString *)string { NSString * test =string; const char *cstr = [test cStringUsingEncoding:NSUTF8StringEncoding ]; NSData *data = [NSData dataWithBytes:cstr length:test.length]; uint8_t digest[CC_SHA1_DIGEST_LENGTH]; CC_SHA1(data.bytes, (int )data.length, digest); NSMutableString * output = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH * 2 ]; for (int i = 0 ; i < CC_SHA1_DIGEST_LENGTH; i++) [output appendFormat:@"%02x" , digest[i]]; return output; } - (NSString *)hmac:(NSString *)plaintext withKey:(NSString *)key { const char *cKey = [key cStringUsingEncoding:NSASCIIStringEncoding ]; const char *cData = [plaintext cStringUsingEncoding:NSASCIIStringEncoding ]; unsigned char cHMAC[CC_SHA1_DIGEST_LENGTH]; CCHmac(kCCHmacAlgSHA1, cKey, strlen(cKey), cData, strlen(cData), cHMAC); NSData *HMACData = [NSData dataWithBytes:cHMAC length:sizeof (cHMAC)]; const unsigned char *buffer = (const unsigned char *)[HMACData bytes]; NSMutableString *HMAC = [NSMutableString stringWithCapacity:HMACData.length * 2 ]; for (int i = 0 ; i < HMACData.length; ++i){ [HMAC appendFormat:@"%02x" , buffer[i]]; } return HMAC; } - (NSString *)md5WithData:(NSData *)data{ unsigned char result[16 ]; CC_MD5( data.bytes, (CC_LONG)data.length, result ); return [NSString stringWithFormat: @"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" , result[0 ], result[1 ], result[2 ], result[3 ], result[4 ], result[5 ], result[6 ], result[7 ], result[8 ], result[9 ], result[10 ], result[11 ], result[12 ], result[13 ], result[14 ], result[15 ] ]; }