BTGraphQLHTTP.m 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. #import "BTGraphQLHTTP.h"
  2. #import "BTURLUtils.h"
  3. #import "Braintree-Version.h"
  4. @interface BTGraphQLHTTP ()
  5. @property (nonatomic, copy) NSString *tokenizationKey;
  6. @property (nonatomic, copy) NSString *authorizationFingerprint;
  7. @end
  8. @implementation BTGraphQLHTTP
  9. static NSString *BraintreeVersion = @"2018-03-06";
  10. #pragma mark - Overrides
  11. - (void)GET:(__unused NSString *)aPath completion:(__unused void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  12. [NSException raise:@"" format:@"GET is unsupported"];
  13. }
  14. - (void)GET:(__unused NSString *)aPath parameters:(__unused NSDictionary *)parameters completion:(__unused void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  15. [NSException raise:@"" format:@"GET is unsupported"];
  16. }
  17. - (void)POST:(__unused NSString *)aPath completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  18. [self httpRequest:@"POST" parameters:nil completion:completionBlock];
  19. }
  20. - (void)POST:(__unused NSString *)aPath parameters:(NSDictionary *)parameters completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  21. [self httpRequest:@"POST" parameters:parameters completion:completionBlock];
  22. }
  23. - (void)PUT:(__unused NSString *)aPath completion:(__unused void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  24. [NSException raise:@"" format:@"PUT is unsupported"];
  25. }
  26. - (void)PUT:(__unused NSString *)aPath parameters:(__unused NSDictionary *)parameters completion:(__unused void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  27. [NSException raise:@"" format:@"PUT is unsupported"];
  28. }
  29. - (void)DELETE:(__unused NSString *)aPath completion:(__unused void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  30. [NSException raise:@"" format:@"DELETE is unsupported"];
  31. }
  32. - (void)DELETE:(__unused NSString *)aPath parameters:(__unused NSDictionary *)parameters completion:(__unused void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  33. [NSException raise:@"" format:@"DELETE is unsupported"];
  34. }
  35. - (void)handleRequestCompletion:(NSData *)data
  36. response:(NSURLResponse *)response
  37. error:(NSError *)error
  38. completionBlock:(void (^)(BTJSON * _Nonnull, NSHTTPURLResponse * _Nonnull, NSError * _Nonnull))completionBlock
  39. {
  40. // Network error
  41. if (error) {
  42. [self callCompletionBlock:completionBlock body:nil response:nil error:error];
  43. return;
  44. }
  45. NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
  46. BTJSON *body = [[BTJSON alloc] initWithValue:json];
  47. // Success case
  48. if ([body asDictionary] && ![body[@"errors"] asArray]) {
  49. [self callCompletionBlock:completionBlock body:body response:(NSHTTPURLResponse *)response error:nil];
  50. return;
  51. }
  52. BTJSON *errorJSON = body[@"errors"][0];
  53. NSString *errorType = [errorJSON[@"extensions"][@"errorType"] asString];
  54. NSInteger statusCode = 0;
  55. BTHTTPErrorCode errorCode = BTHTTPErrorCodeUnknown;
  56. NSMutableDictionary *errorBody = [NSMutableDictionary new];
  57. if ([errorType isEqualToString:@"user_error"]) {
  58. statusCode = 422;
  59. errorCode = BTHTTPErrorCodeClientError;
  60. errorBody[@"error"] = @{@"message": @"Input is invalid"};
  61. NSMutableArray *errors = [NSMutableArray new];
  62. NSUInteger errorCount = [body[@"errors"] asArray].count;
  63. for (NSUInteger i = 0; i < errorCount; i++) {
  64. BTJSON *error = body[@"errors"][i];
  65. NSArray *inputPath = [error[@"extensions"][@"inputPath"] asStringArray];
  66. // Defensive programming
  67. if (!inputPath) {
  68. continue;
  69. }
  70. [self addErrorForInputPath:[inputPath subarrayWithRange:NSMakeRange(1, inputPath.count - 1)]
  71. withGraphQLError:[error asDictionary]
  72. toArray:errors];
  73. }
  74. if (errors.count > 0) {
  75. errorBody[@"fieldErrors"] = [errors copy];
  76. }
  77. } else if ([errorType isEqualToString:@"developer_error"]) {
  78. statusCode = 403;
  79. errorCode = BTHTTPErrorCodeClientError;
  80. if ([errorJSON[@"message"] asString]) {
  81. errorBody[@"error"] = @{@"message": [errorJSON[@"message"] asString]};
  82. }
  83. } else {
  84. statusCode = 500;
  85. errorCode = BTHTTPErrorCodeServerError;
  86. errorBody[@"error"] = @{@"message": @"An unexpected error occurred"};
  87. }
  88. NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
  89. NSHTTPURLResponse *nestedErrorResponse = [[NSHTTPURLResponse alloc] initWithURL:response.URL statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:httpResponse.allHeaderFields];
  90. // Create errors
  91. NSError *returnedError = [[NSError alloc] initWithDomain:BTHTTPErrorDomain
  92. code:errorCode
  93. userInfo:@{
  94. BTHTTPURLResponseKey: nestedErrorResponse,
  95. BTHTTPJSONResponseBodyKey: [[BTJSON alloc] initWithValue:[errorBody copy]]
  96. }];
  97. [self callCompletionBlock:completionBlock body:[[BTJSON alloc] initWithValue:[errorBody copy]] response:(NSHTTPURLResponse *)response error:returnedError];
  98. }
  99. #pragma mark - Private methods
  100. - (void)httpRequest:(NSString *)method
  101. parameters:(NSDictionary *)parameters
  102. completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock
  103. {
  104. if (!self.baseURL || [self.baseURL.absoluteString isEqualToString:@""]) {
  105. NSMutableDictionary *errorUserInfo = [NSMutableDictionary new];
  106. if (method) errorUserInfo[@"method"] = method;
  107. if (parameters) errorUserInfo[@"parameters"] = parameters;
  108. completionBlock(nil, nil, [NSError errorWithDomain:BTHTTPErrorDomain code:BTHTTPErrorCodeMissingBaseURL userInfo:errorUserInfo]);
  109. return;
  110. }
  111. NSURLComponents *components = [NSURLComponents componentsWithString:self.baseURL.absoluteString];
  112. NSMutableDictionary *headers = [NSMutableDictionary dictionaryWithDictionary:@{
  113. @"User-Agent": [self userAgentString],
  114. @"Braintree-Version": BraintreeVersion
  115. }];
  116. headers[@"Authorization"] = [NSString stringWithFormat:@"Bearer %@", self.authorizationFingerprint ?: self.tokenizationKey];
  117. parameters = parameters ? [NSMutableDictionary dictionaryWithDictionary:parameters] : [NSMutableDictionary new];
  118. NSMutableURLRequest *request;
  119. headers[@"Content-Type"] = @"application/json; charset=utf-8";
  120. NSError *jsonSerializationError;
  121. NSData *bodyData = [NSJSONSerialization dataWithJSONObject:parameters
  122. options:0
  123. error:&jsonSerializationError];
  124. if (jsonSerializationError) {
  125. completionBlock(nil, nil, jsonSerializationError);
  126. return;
  127. }
  128. request = [NSMutableURLRequest requestWithURL:components.URL];
  129. [request setHTTPBody:bodyData];
  130. [request setAllHTTPHeaderFields:headers];
  131. [request setHTTPMethod:method];
  132. // Perform the actual request
  133. NSURLSessionTask *task = [self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
  134. if (error) {
  135. [self callCompletionBlock:completionBlock body:nil response:(NSHTTPURLResponse *)response error:error];
  136. return;
  137. }
  138. [self handleRequestCompletion:data response:response error:error completionBlock:completionBlock];
  139. }];
  140. [task resume];
  141. }
  142. /// Walks through the input path recursively and adds field errors to a mutable array
  143. - (void)addErrorForInputPath:(NSArray <NSString *> *)inputPath withGraphQLError:(NSDictionary *)errorJSON toArray:(NSMutableArray <NSDictionary *> *)errors {
  144. NSString *field = [inputPath firstObject];
  145. if (inputPath.count == 1) {
  146. [errors addObject:@{
  147. @"field": field,
  148. @"message": errorJSON[@"message"],
  149. @"code": errorJSON[@"extensions"][@"legacyCode"]
  150. }];
  151. return;
  152. }
  153. // Find nested error that matches the field
  154. NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(field == %@)", field];
  155. NSDictionary *nestedFieldError = [[errors filteredArrayUsingPredicate:predicate] firstObject];
  156. // If the nested error hasn't been created yet, add one
  157. if (!nestedFieldError) {
  158. nestedFieldError = @{
  159. @"field": field,
  160. @"fieldErrors": [NSMutableArray new]
  161. };
  162. [errors addObject:nestedFieldError];
  163. }
  164. [self addErrorForInputPath:[inputPath subarrayWithRange:NSMakeRange(1, inputPath.count - 1)]
  165. withGraphQLError:errorJSON
  166. toArray:nestedFieldError[@"fieldErrors"]];
  167. }
  168. @end