BTGraphQLHTTP.m 10 KB

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