Skip to content

Commit 8813381

Browse files
committed
Refactor HTTP headers handling (#931)
* Add `CaseInsensitiveMap` class * Add `HttpHeaders` class * Use `HttpHeaders` in `StripeRequest` * Use `HttpHeaders` in `StripeResponse` * Address review comments
1 parent 5858a79 commit 8813381

13 files changed

+659
-187
lines changed

src/main/java/com/stripe/net/HttpClient.java

+7-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.time.Duration;
99
import java.util.HashMap;
1010
import java.util.Map;
11+
import java.util.Optional;
1112
import java.util.concurrent.ThreadLocalRandom;
1213

1314
/** Base abstract class for HTTP clients used to send requests to Stripe's API. */
@@ -57,7 +58,11 @@ public HttpClient(int maxNetworkRetries) {
5758
* @throws StripeException If the request fails for any reason
5859
*/
5960
public StripeResponse requestWithTelemetry(StripeRequest request) throws StripeException {
60-
requestTelemetry.maybeAddTelemetryHeader(request.headers());
61+
Optional<String> telemetryHeaderValue = requestTelemetry.getHeaderValue(request.headers());
62+
if (telemetryHeaderValue.isPresent()) {
63+
request =
64+
request.withAdditionalHeader(RequestTelemetry.HEADER_NAME, telemetryHeaderValue.get());
65+
}
6166

6267
Stopwatch stopwatch = Stopwatch.startNew();
6368

@@ -189,7 +194,7 @@ private boolean shouldRetry(int numRetries, StripeException exception, StripeRes
189194
// The API may ask us not to retry (eg; if doing so would be a no-op)
190195
// or advise us to retry (eg; in cases of lock timeouts); we defer to that.
191196
if ((response != null) && (response.headers() != null)) {
192-
String value = response.headers().get("Stripe-Should-Retry");
197+
String value = response.headers().firstValue("Stripe-Should-Retry").orElse(null);
193198

194199
if ("true".equals(value)) {
195200
return true;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package com.stripe.net;
2+
3+
import static java.util.Objects.requireNonNull;
4+
5+
import com.stripe.util.CaseInsensitiveMap;
6+
import java.util.Arrays;
7+
import java.util.Collections;
8+
import java.util.HashMap;
9+
import java.util.List;
10+
import java.util.Map;
11+
import java.util.Optional;
12+
import lombok.EqualsAndHashCode;
13+
14+
/**
15+
* A read-only view of a set of HTTP headers.
16+
*
17+
* <p>This class mimics the {@code java.net.http.HttpHeaders} added in Java 11.
18+
*/
19+
@EqualsAndHashCode
20+
public class HttpHeaders {
21+
private CaseInsensitiveMap<List<String>> headerMap;
22+
23+
private HttpHeaders(CaseInsensitiveMap<List<String>> headerMap) {
24+
this.headerMap = headerMap;
25+
}
26+
27+
/**
28+
* Returns an {@link HttpHeaders} instance initialized from the given map.
29+
*
30+
* @param headerMap the map containing the header names and values
31+
* @return an {@link HttpHeaders} instance containing the given headers
32+
* @throws NullPointerException if {@code headerMap} is {@code null}
33+
*/
34+
public static HttpHeaders of(Map<String, List<String>> headerMap) {
35+
requireNonNull(headerMap);
36+
return new HttpHeaders(CaseInsensitiveMap.of(headerMap));
37+
}
38+
39+
/**
40+
* Returns a new {@link HttpHeaders} instance containing the headers of the current instance plus
41+
* the provided header.
42+
*
43+
* @param name the name of the header to add
44+
* @param value the value of the header to add
45+
* @return the new {@link HttpHeaders} instance
46+
* @throws NullPointerException if {@code name} or {@code value} is {@code null}
47+
*/
48+
public HttpHeaders withAdditionalHeader(String name, String value) {
49+
requireNonNull(name);
50+
requireNonNull(value);
51+
return this.withAdditionalHeader(name, Arrays.asList(value));
52+
}
53+
54+
/**
55+
* Returns a new {@link HttpHeaders} instance containing the headers of the current instance plus
56+
* the provided header.
57+
*
58+
* @param name the name of the header to add
59+
* @param values the values of the header to add
60+
* @return the new {@link HttpHeaders} instance
61+
* @throws NullPointerException if {@code name} or {@code values} is {@code null}
62+
*/
63+
public HttpHeaders withAdditionalHeader(String name, List<String> values) {
64+
requireNonNull(name);
65+
requireNonNull(values);
66+
Map<String, List<String>> headerMap = new HashMap<>();
67+
headerMap.put(name, values);
68+
return this.withAdditionalHeaders(headerMap);
69+
}
70+
71+
/**
72+
* Returns a new {@link HttpHeaders} instance containing the headers of the current instance plus
73+
* the provided headers.
74+
*
75+
* @param headerMap the map containing the headers to add
76+
* @return the new {@link HttpHeaders} instance
77+
* @throws NullPointerException if {@code headerMap} is {@code null}
78+
*/
79+
public HttpHeaders withAdditionalHeaders(Map<String, List<String>> headerMap) {
80+
requireNonNull(headerMap);
81+
Map<String, List<String>> newHeaderMap = new HashMap<>(this.map());
82+
newHeaderMap.putAll(headerMap);
83+
return HttpHeaders.of(newHeaderMap);
84+
}
85+
86+
/**
87+
* Returns an unmodifiable List of all of the header string values of the given named header.
88+
* Always returns a List, which may be empty if the header is not present.
89+
*
90+
* @param name the header name
91+
* @return a List of headers string values
92+
*/
93+
public List<String> allValues(String name) {
94+
if (this.headerMap.containsKey(name)) {
95+
List<String> values = this.headerMap.get(name);
96+
if ((values != null) && (values.size() > 0)) {
97+
return Collections.unmodifiableList(values);
98+
}
99+
}
100+
return Collections.emptyList();
101+
}
102+
103+
/**
104+
* Returns an {@link Optional} containing the first header string value of the given named (and
105+
* possibly multi-valued) header. If the header is not present, then the returned {@code Optional}
106+
* is empty.
107+
*
108+
* @param name the header name
109+
* @return an {@code Optional<String>} containing the first named header string value, if present
110+
*/
111+
public Optional<String> firstValue(String name) {
112+
if (this.headerMap.containsKey(name)) {
113+
List<String> values = this.headerMap.get(name);
114+
if ((values != null) && (values.size() > 0)) {
115+
return Optional.of(values.get(0));
116+
}
117+
}
118+
return Optional.empty();
119+
}
120+
121+
/**
122+
* Returns an unmodifiable Map view of this HttpHeaders.
123+
*
124+
* @return the Map
125+
*/
126+
public Map<String, List<String>> map() {
127+
return Collections.unmodifiableMap(this.headerMap);
128+
}
129+
130+
/**
131+
* Returns this {@link HttpHeaders} as a string.
132+
*
133+
* @return a string describing the HTTP headers
134+
*/
135+
@Override
136+
public String toString() {
137+
StringBuilder sb = new StringBuilder();
138+
sb.append(super.toString());
139+
sb.append(" { ");
140+
sb.append(map());
141+
sb.append(" }");
142+
return sb.toString();
143+
}
144+
}

src/main/java/com/stripe/net/HttpURLConnectionClient.java

+9-12
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.net.Authenticator;
99
import java.net.HttpURLConnection;
1010
import java.net.PasswordAuthentication;
11+
import java.util.Arrays;
1112
import java.util.HashMap;
1213
import java.util.List;
1314
import java.util.Map;
@@ -78,18 +79,14 @@ public StripeResponse request(StripeRequest request) throws ApiConnectionExcepti
7879
}
7980
}
8081

81-
static Map<String, String> getHeaders(StripeRequest request) {
82-
Map<String, String> headers = new HashMap<>();
82+
static HttpHeaders getHeaders(StripeRequest request) {
83+
Map<String, List<String>> userAgentHeadersMap = new HashMap<>();
8384

84-
headers.putAll(request.headers());
85+
userAgentHeadersMap.put("User-Agent", Arrays.asList(buildUserAgentString()));
86+
userAgentHeadersMap.put(
87+
"X-Stripe-Client-User-Agent", Arrays.asList(buildXStripeClientUserAgentString()));
8588

86-
headers.put("Accept-Charset", ApiResource.CHARSET);
87-
headers.put("Accept", "application/json");
88-
89-
headers.put("User-Agent", buildUserAgentString());
90-
headers.put("X-Stripe-Client-User-Agent", buildXStripeClientUserAgentString());
91-
92-
return headers;
89+
return request.headers().withAdditionalHeaders(userAgentHeadersMap);
9390
}
9491

9592
private static HttpURLConnection createStripeConnection(StripeRequest request)
@@ -112,8 +109,8 @@ protected PasswordAuthentication getPasswordAuthentication() {
112109
conn.setConnectTimeout(request.options().getConnectTimeout());
113110
conn.setReadTimeout(request.options().getReadTimeout());
114111
conn.setUseCaches(false);
115-
for (Map.Entry<String, String> header : getHeaders(request).entrySet()) {
116-
conn.setRequestProperty(header.getKey(), header.getValue());
112+
for (Map.Entry<String, List<String>> entry : getHeaders(request).map().entrySet()) {
113+
conn.setRequestProperty(entry.getKey(), String.join(",", entry.getValue()));
117114
}
118115

119116
conn.setRequestMethod(request.method().name());

src/main/java/com/stripe/net/RequestTelemetry.java

+13-9
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
import com.google.gson.annotations.SerializedName;
55
import com.stripe.Stripe;
66
import java.time.Duration;
7-
import java.util.Map;
7+
import java.util.Optional;
88
import java.util.concurrent.ConcurrentLinkedQueue;
99
import lombok.Data;
1010

1111
/** Helper class used by {@link LiveStripeResponseGetter} to manage request telemetry. */
1212
class RequestTelemetry {
13+
/** The name of the header used to send request telemetry in requests. */
14+
public static final String HEADER_NAME = "X-Stripe-Client-Telemetry";
15+
1316
private static final int MAX_REQUEST_METRICS_QUEUE_SIZE = 100;
1417

1518
private static final Gson gson = new Gson();
@@ -18,27 +21,28 @@ class RequestTelemetry {
1821
new ConcurrentLinkedQueue<RequestMetrics>();
1922

2023
/**
21-
* If telemetry is enabled and there is at least one metrics item in the queue, then add a {@code
22-
* X-Stripe-Client-Telemetry} header with the item; otherwise, do nothing.
24+
* Returns an {@link Optional} containing the value of the {@code X-Stripe-Telemetry} header to
25+
* add to the request. If the header is already present in the request, or if there is available
26+
* metrics, or if telemetry is disabled, then the returned {@code Optional} is empty.
2327
*
2428
* @param headers the request headers
2529
*/
26-
public void maybeAddTelemetryHeader(Map<String, String> headers) {
27-
if (headers.containsKey("X-Stripe-Telemetry")) {
28-
return;
30+
public Optional<String> getHeaderValue(HttpHeaders headers) {
31+
if (headers.firstValue(HEADER_NAME).isPresent()) {
32+
return Optional.empty();
2933
}
3034

3135
RequestMetrics requestMetrics = prevRequestMetrics.poll();
3236
if (requestMetrics == null) {
33-
return;
37+
return Optional.empty();
3438
}
3539

3640
if (!Stripe.enableTelemetry) {
37-
return;
41+
return Optional.empty();
3842
}
3943

4044
ClientTelemetryPayload payload = new ClientTelemetryPayload(requestMetrics);
41-
headers.put("X-Stripe-Client-Telemetry", gson.toJson(payload));
45+
return Optional.of(gson.toJson(payload));
4246
}
4347

4448
/**

src/main/java/com/stripe/net/StripeHeaders.java

-50
This file was deleted.

0 commit comments

Comments
 (0)