• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stripe / stripe-java / #16493

03 Oct 2024 07:15PM UTC coverage: 12.942% (+0.08%) from 12.864%
#16493

push

github

web-flow
Merge Stripe-java v27.0.0 to beta branch (#1888)

409 of 1651 new or added lines in 88 files covered. (24.77%)

31 existing lines in 7 files now uncovered.

18773 of 145050 relevant lines covered (12.94%)

0.13 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.13
/src/main/java/com/stripe/net/LiveStripeResponseGetter.java
1
package com.stripe.net;
2

3
import com.google.gson.JsonElement;
4
import com.google.gson.JsonObject;
5
import com.google.gson.JsonPrimitive;
6
import com.google.gson.JsonSyntaxException;
7
import com.stripe.Stripe;
8
import com.stripe.exception.*;
9
import com.stripe.exception.ApiKeyMissingException;
10
import com.stripe.exception.oauth.InvalidClientException;
11
import com.stripe.exception.oauth.InvalidGrantException;
12
import com.stripe.exception.oauth.InvalidScopeException;
13
import com.stripe.exception.oauth.OAuthException;
14
import com.stripe.exception.oauth.UnsupportedGrantTypeException;
15
import com.stripe.exception.oauth.UnsupportedResponseTypeException;
16
import com.stripe.model.*;
17
import com.stripe.model.oauth.OAuthError;
18
import com.stripe.util.Stopwatch;
19
import java.io.IOException;
20
import java.io.InputStream;
21
import java.lang.reflect.Type;
22
import java.util.List;
23
import java.util.Map;
24
import java.util.Optional;
25

26
public class LiveStripeResponseGetter implements StripeResponseGetter {
27
  private final HttpClient httpClient;
28
  private final StripeResponseGetterOptions options;
29

30
  private final RequestTelemetry requestTelemetry = new RequestTelemetry();
1✔
31

32
  @FunctionalInterface
33
  private interface RequestSendFunction<R> {
34
    R apply(StripeRequest request) throws StripeException;
35
  }
36

37
  private <T extends AbstractStripeResponse<?>> T sendWithTelemetry(
38
      StripeRequest request, List<String> usage, RequestSendFunction<T> send)
39
      throws StripeException {
40

41
    Stopwatch stopwatch = Stopwatch.startNew();
1✔
42

43
    T response = send.apply(request);
1✔
44

45
    stopwatch.stop();
1✔
46

47
    requestTelemetry.maybeEnqueueMetrics(response, stopwatch.getElapsed(), usage);
1✔
48

49
    return response;
1✔
50
  }
51

52
  /**
53
   * Initializes a new instance of the {@link LiveStripeResponseGetter} class with default
54
   * parameters.
55
   */
56
  public LiveStripeResponseGetter() {
57
    this(null, null);
1✔
58
  }
1✔
59

60
  /**
61
   * Initializes a new instance of the {@link LiveStripeResponseGetter} class.
62
   *
63
   * @param httpClient the HTTP client to use
64
   */
65
  public LiveStripeResponseGetter(HttpClient httpClient) {
66
    this(null, httpClient);
1✔
67
  }
1✔
68

69
  /**
70
   * Initializes a new instance of the {@link LiveStripeResponseGetter} class.
71
   *
72
   * @param options the client options instance to use
73
   * @param httpClient the HTTP client to use
74
   */
75
  public LiveStripeResponseGetter(StripeResponseGetterOptions options, HttpClient httpClient) {
1✔
76
    this.options = options != null ? options : GlobalStripeResponseGetterOptions.INSTANCE;
1✔
77
    this.httpClient = (httpClient != null) ? httpClient : buildDefaultHttpClient();
1✔
78
  }
1✔
79

80
  private StripeRequest toStripeRequest(ApiRequest apiRequest, RequestOptions mergedOptions)
81
      throws StripeException {
82
    String fullUrl = fullUrl(apiRequest);
1✔
83

84
    Optional<String> telemetryHeaderValue = requestTelemetry.pollPayload();
1✔
85
    StripeRequest request =
1✔
86
        StripeRequest.create(
1✔
87
            apiRequest.getMethod(),
1✔
88
            fullUrl,
89
            apiRequest.getParams(),
1✔
90
            mergedOptions,
91
            apiRequest.getApiMode());
1✔
92
    if (telemetryHeaderValue.isPresent()) {
1✔
93
      request =
1✔
94
          request.withAdditionalHeader(RequestTelemetry.HEADER_NAME, telemetryHeaderValue.get());
1✔
95
    }
96
    return request;
1✔
97
  }
98

99
  private StripeRequest toRawStripeRequest(RawApiRequest apiRequest, RequestOptions mergedOptions)
100
      throws StripeException {
101
    String fullUrl = fullUrl(apiRequest);
1✔
102

103
    Optional<String> telemetryHeaderValue = requestTelemetry.pollPayload();
1✔
104
    StripeRequest request =
1✔
105
        StripeRequest.createWithStringContent(
1✔
106
            apiRequest.getMethod(),
1✔
107
            fullUrl,
108
            apiRequest.getRawContent(),
1✔
109
            mergedOptions,
110
            apiRequest.getApiMode());
1✔
111

112
    if (telemetryHeaderValue.isPresent()) {
1✔
113
      request =
1✔
114
          request.withAdditionalHeader(RequestTelemetry.HEADER_NAME, telemetryHeaderValue.get());
1✔
115
    }
116
    return request;
1✔
117
  }
118

119
  @Override
120
  @SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"})
121
  public <T extends StripeObjectInterface> T request(ApiRequest apiRequest, Type typeToken)
122
      throws StripeException {
123

124
    RequestOptions mergedOptions = RequestOptions.merge(this.options, apiRequest.getOptions());
1✔
125

126
    if (RequestOptions.unsafeGetStripeVersionOverride(mergedOptions) != null) {
1✔
127
      apiRequest = apiRequest.addUsage("unsafe_stripe_version_override");
1✔
128
    }
129

130
    StripeRequest request = toStripeRequest(apiRequest, mergedOptions);
1✔
131
    StripeResponse response =
1✔
132
        sendWithTelemetry(request, apiRequest.getUsage(), r -> httpClient.requestWithRetries(r));
1✔
133

134
    int responseCode = response.code();
1✔
135
    String responseBody = response.body();
1✔
136
    String requestId = response.requestId();
1✔
137

138
    if (responseCode < 200 || responseCode >= 300) {
1✔
NEW
139
      handleError(response, apiRequest.getApiMode());
×
140
    }
141

142
    T resource = null;
1✔
143
    try {
144
      resource = (T) ApiResource.deserializeStripeObject(responseBody, typeToken, this);
1✔
145
    } catch (JsonSyntaxException e) {
1✔
NEW
146
      throw makeMalformedJsonError(responseBody, responseCode, requestId, e);
×
147
    }
1✔
148

149
    if (resource instanceof StripeCollectionInterface<?>) {
1✔
150
      ((StripeCollectionInterface<?>) resource).setRequestOptions(apiRequest.getOptions());
1✔
151
      ((StripeCollectionInterface<?>) resource).setRequestParams(apiRequest.getParams());
1✔
152
    }
153

154
    if (resource instanceof com.stripe.model.v2.StripeCollection<?>) {
1✔
155
      ((com.stripe.model.v2.StripeCollection<?>) resource)
1✔
156
          .setRequestOptions(apiRequest.getOptions());
1✔
157
    }
158

159
    resource.setLastResponse(response);
1✔
160

161
    return resource;
1✔
162
  }
163

164
  @Override
165
  public InputStream requestStream(ApiRequest apiRequest) throws StripeException {
166
    RequestOptions mergedOptions = RequestOptions.merge(this.options, apiRequest.getOptions());
1✔
167

168
    if (RequestOptions.unsafeGetStripeVersionOverride(mergedOptions) != null) {
1✔
169
      apiRequest = apiRequest.addUsage("unsafe_stripe_version_override");
×
170
    }
171

172
    StripeRequest request = toStripeRequest(apiRequest, mergedOptions);
1✔
173
    StripeResponseStream responseStream =
1✔
174
        sendWithTelemetry(
1✔
175
            request, apiRequest.getUsage(), r -> httpClient.requestStreamWithRetries(r));
1✔
176

177
    int responseCode = responseStream.code();
1✔
178

179
    if (responseCode < 200 || responseCode >= 300) {
1✔
180
      StripeResponse response;
181
      try {
182
        response = responseStream.unstream();
×
183
      } catch (IOException e) {
×
184
        throw new ApiConnectionException(
×
185
            String.format(
×
186
                "IOException during API request to Stripe (%s): %s "
187
                    + "Please check your internet connection and try again. If this problem persists,"
188
                    + "you should check Stripe's service status at https://twitter.com/stripestatus,"
189
                    + " or let us know at support@stripe.com.",
190
                Stripe.getApiBase(), e.getMessage()),
×
191
            e);
192
      }
×
NEW
193
      handleError(response, apiRequest.getApiMode());
×
194
    }
195

196
    return responseStream.body();
1✔
197
  }
198

199
  @Override
200
  public StripeResponse rawRequest(RawApiRequest apiRequest) throws StripeException {
201
    RequestOptions mergedOptions = RequestOptions.merge(this.options, apiRequest.getOptions());
1✔
202

203
    if (RequestOptions.unsafeGetStripeVersionOverride(mergedOptions) != null) {
1✔
204
      apiRequest = apiRequest.addUsage("unsafe_stripe_version_override");
×
205
    }
206

207
    StripeRequest request = toRawStripeRequest(apiRequest, mergedOptions);
1✔
208

209
    Map<String, String> additionalHeaders = apiRequest.getOptions().getAdditionalHeaders();
1✔
210

211
    if (additionalHeaders != null) {
1✔
212
      for (Map.Entry<String, String> entry : additionalHeaders.entrySet()) {
1✔
213
        String key = entry.getKey();
1✔
214
        String value = entry.getValue();
1✔
215
        request = request.withAdditionalHeader(key, value);
1✔
216
      }
1✔
217
    }
218

219
    StripeResponse response =
1✔
220
        sendWithTelemetry(request, apiRequest.getUsage(), r -> httpClient.requestWithRetries(r));
1✔
221

222
    int responseCode = response.code();
1✔
223

224
    if (responseCode < 200 || responseCode >= 300) {
1✔
NEW
225
      handleError(response, apiRequest.getApiMode());
×
226
    }
227

228
    return response;
1✔
229
  }
230

231
  @Override
232
  @SuppressWarnings({"TypeParameterUnusedInFormals", "deprecation"})
233
  public <T extends StripeObjectInterface> T request(
234
      BaseAddress baseAddress,
235
      ApiResource.RequestMethod method,
236
      String path,
237
      Map<String, Object> params,
238
      Type typeToken,
239
      RequestOptions options,
240
      ApiMode apiMode)
241
      throws StripeException {
242
    return this.request(new ApiRequest(baseAddress, method, path, params, options), typeToken);
×
243
  }
244

245
  @Override
246
  @SuppressWarnings({"TypeParameterUnusedInFormals", "deprecation"})
247
  public InputStream requestStream(
248
      BaseAddress baseAddress,
249
      ApiResource.RequestMethod method,
250
      String path,
251
      Map<String, Object> params,
252
      RequestOptions options,
253
      ApiMode apiMode)
254
      throws StripeException {
UNCOV
255
    return this.requestStream(new ApiRequest(baseAddress, method, path, params, options));
×
256
  }
257

258
  private static HttpClient buildDefaultHttpClient() {
259
    return new HttpURLConnectionClient();
1✔
260
  }
261

262
  private static ApiException makeMalformedJsonError(
263
      String responseBody, int responseCode, String requestId, Throwable e) throws ApiException {
264
    String details = e == null ? "none" : e.getMessage();
1✔
265
    throw new ApiException(
1✔
266
        String.format(
1✔
267
            "Invalid response object from API: %s. (HTTP response code was %d). Additional details: %s.",
268
            responseBody, responseCode, details),
1✔
269
        requestId,
270
        null,
271
        responseCode,
1✔
272
        e);
273
  }
274

275
  private StripeError parseStripeError(
276
      String body, int code, String requestId, Class<? extends StripeError> klass)
277
      throws StripeException {
278
    StripeError ret;
279
    try {
280
      JsonObject jsonObject =
1✔
281
          ApiResource.GSON.fromJson(body, JsonObject.class).getAsJsonObject("error");
1✔
282
      ret = (StripeError) StripeObject.deserializeStripeObject(jsonObject, klass, this);
1✔
283
      if (ret != null) return ret;
1✔
NEW
284
    } catch (JsonSyntaxException e) {
×
NEW
285
      throw makeMalformedJsonError(body, code, requestId, e);
×
NEW
286
    }
×
NEW
287
    throw makeMalformedJsonError(body, code, requestId, null);
×
288
  }
289

290
  private void handleError(StripeResponse response, ApiMode apiMode) throws StripeException {
291
    JsonObject responseBody = ApiResource.GSON.fromJson(response.body(), JsonObject.class);
1✔
292

293
    /*
294
     OAuth errors are JSON objects where `error` is a string. In
295
     contrast, in API errors, `error` is a hash with sub-keys. We use
296
     this property to distinguish between OAuth and API errors.
297
    */
298
    if (responseBody.has("error") && responseBody.get("error").isJsonPrimitive()) {
1✔
299
      JsonPrimitive error = responseBody.getAsJsonPrimitive("error");
1✔
300
      if (error.isString()) {
1✔
301
        handleOAuthError(response);
×
302
      }
303
    } else if (apiMode == ApiMode.V2) {
1✔
NEW
304
      handleV2ApiError(response);
×
305
    } else {
NEW
306
      handleV1ApiError(response);
×
307
    }
308
  }
×
309

310
  private void handleV1ApiError(StripeResponse response) throws StripeException {
311
    StripeException exception = null;
1✔
312

313
    StripeError error =
1✔
314
        parseStripeError(response.body(), response.code(), response.requestId(), StripeError.class);
1✔
315

316
    error.setLastResponse(response);
1✔
317
    switch (response.code()) {
1✔
318
      case 400:
319
      case 404:
320
        if ("idempotency_error".equals(error.getType())) {
1✔
321
          exception =
1✔
322
              new IdempotencyException(
323
                  error.getMessage(), response.requestId(), error.getCode(), response.code());
1✔
324
        } else {
325
          exception =
1✔
326
              new InvalidRequestException(
327
                  error.getMessage(),
1✔
328
                  error.getParam(),
1✔
329
                  response.requestId(),
1✔
330
                  error.getCode(),
1✔
331
                  response.code(),
1✔
332
                  null);
333
        }
334
        break;
1✔
335
      case 401:
336
        exception =
×
337
            new AuthenticationException(
338
                error.getMessage(), response.requestId(), error.getCode(), response.code());
×
339
        break;
×
340
      case 402:
341
        exception =
×
342
            new CardException(
343
                error.getMessage(),
×
344
                response.requestId(),
×
345
                error.getCode(),
×
346
                error.getParam(),
×
347
                error.getDeclineCode(),
×
348
                error.getCharge(),
×
349
                response.code(),
×
350
                null);
351
        break;
×
352
      case 403:
353
        exception =
×
354
            new PermissionException(
355
                error.getMessage(), response.requestId(), error.getCode(), response.code());
×
356
        break;
×
357
      case 429:
358
        exception =
×
359
            new RateLimitException(
360
                error.getMessage(),
×
361
                error.getParam(),
×
362
                response.requestId(),
×
363
                error.getCode(),
×
364
                response.code(),
×
365
                null);
366
        break;
×
367
      default:
368
        exception =
×
369
            new ApiException(
370
                error.getMessage(), response.requestId(), error.getCode(), response.code(), null);
×
371
        break;
372
    }
373
    exception.setStripeError(error);
1✔
374

375
    throw exception;
1✔
376
  }
377

378
  private void handleV2ApiError(StripeResponse response) throws StripeException {
379
    JsonObject body =
1✔
380
        ApiResource.GSON.fromJson(response.body(), JsonObject.class).getAsJsonObject("error");
1✔
381

382
    JsonElement typeElement = body == null ? null : body.get("type");
1✔
383
    JsonElement codeElement = body == null ? null : body.get("code");
1✔
384
    String type = typeElement == null ? "<no_type>" : typeElement.getAsString();
1✔
385
    String code = codeElement == null ? "<no_code>" : codeElement.getAsString();
1✔
386

387
    StripeException exception =
1✔
388
        StripeException.parseV2Exception(type, body, response.code(), response.requestId(), this);
1✔
389
    if (exception != null) {
1✔
390
      throw exception;
1✔
391
    }
392

393
    StripeError error;
394
    try {
395
      error =
1✔
396
          parseStripeError(
1✔
397
              response.body(), response.code(), response.requestId(), StripeError.class);
1✔
398
    } catch (ApiException e) {
1✔
399
      String message = "Unrecognized error type '" + type + "'";
1✔
400
      JsonElement messageField = body == null ? null : body.get("message");
1✔
401
      if (messageField != null && messageField.isJsonPrimitive()) {
1✔
NEW
402
        message = messageField.getAsString();
×
403
      }
404

405
      throw new ApiException(message, response.requestId(), code, response.code(), null);
1✔
406
    }
1✔
407

408
    error.setLastResponse(response);
1✔
409
    exception =
1✔
410
        new ApiException(error.getMessage(), response.requestId(), code, response.code(), null);
1✔
411
    exception.setStripeError(error);
1✔
412
    throw exception;
1✔
413
  }
414

415
  private void handleOAuthError(StripeResponse response) throws StripeException {
416
    OAuthError error = null;
1✔
417
    StripeException exception = null;
1✔
418

419
    try {
420
      error = StripeObject.deserializeStripeObject(response.body(), OAuthError.class, this);
1✔
421
    } catch (JsonSyntaxException e) {
×
NEW
422
      throw makeMalformedJsonError(response.body(), response.code(), response.requestId(), e);
×
423
    }
1✔
424
    if (error == null) {
1✔
NEW
425
      throw makeMalformedJsonError(response.body(), response.code(), response.requestId(), null);
×
426
    }
427

428
    error.setLastResponse(response);
1✔
429

430
    String code = error.getError();
1✔
431
    String description = (error.getErrorDescription() != null) ? error.getErrorDescription() : code;
1✔
432

433
    switch (code) {
1✔
434
      case "invalid_client":
435
        exception =
1✔
436
            new InvalidClientException(
437
                code, description, response.requestId(), response.code(), null);
1✔
438
        break;
1✔
439
      case "invalid_grant":
440
        exception =
×
441
            new InvalidGrantException(
442
                code, description, response.requestId(), response.code(), null);
×
443
        break;
×
444
      case "invalid_request":
445
        exception =
×
446
            new com.stripe.exception.oauth.InvalidRequestException(
447
                code, description, response.requestId(), response.code(), null);
×
448
        break;
×
449
      case "invalid_scope":
450
        exception =
×
451
            new InvalidScopeException(
452
                code, description, response.requestId(), response.code(), null);
×
453
        break;
×
454
      case "unsupported_grant_type":
455
        exception =
×
456
            new UnsupportedGrantTypeException(
457
                code, description, response.requestId(), response.code(), null);
×
458
        break;
×
459
      case "unsupported_response_type":
460
        exception =
×
461
            new UnsupportedResponseTypeException(
462
                code, description, response.requestId(), response.code(), null);
×
463
        break;
×
464
      default:
465
        exception = new ApiException(code, response.requestId(), null, response.code(), null);
×
466
        break;
467
    }
468

469
    if (exception instanceof OAuthException) {
1✔
470
      ((OAuthException) exception).setOauthError(error);
1✔
471
    }
472

473
    throw exception;
1✔
474
  }
475

476
  @Override
477
  public void validateRequestOptions(RequestOptions options) {
478
    if ((options == null || options.getAuthenticator() == null)
1✔
479
        && this.options.getAuthenticator() == null) {
1✔
480
      throw new ApiKeyMissingException(
1✔
481
          "API key is not set. You can set the API key globally using Stripe.ApiKey, or by passing RequestOptions");
482
    }
483
  }
1✔
484

485
  private String fullUrl(BaseApiRequest apiRequest) {
486
    BaseAddress baseAddress = apiRequest.getBaseAddress();
1✔
487
    RequestOptions options = apiRequest.getOptions();
1✔
488
    String relativeUrl = apiRequest.getPath();
1✔
489
    String baseUrl;
490
    switch (baseAddress) {
1✔
491
      case API:
492
        baseUrl = this.options.getApiBase();
1✔
493
        break;
1✔
494
      case CONNECT:
495
        baseUrl = this.options.getConnectBase();
1✔
496
        break;
1✔
497
      case FILES:
498
        baseUrl = this.options.getFilesBase();
1✔
499
        break;
1✔
500
      case METER_EVENTS:
NEW
501
        baseUrl = this.options.getMeterEventsBase();
×
NEW
502
        break;
×
503
      default:
504
        throw new IllegalArgumentException("Unknown base address " + baseAddress);
×
505
    }
506
    if (options != null && options.getBaseUrl() != null) {
1✔
507
      baseUrl = options.getBaseUrl();
1✔
508
    }
509
    return String.format("%s%s", baseUrl, relativeUrl);
1✔
510
  }
511
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc