Skip to content

Commit 14d386b

Browse files
douglasmillerclaude
andcommitted
fix: Switch DefaultHttpAdapter from OkHttp to HttpURLConnection
Replace OkHttp transport with java.net.HttpURLConnection; remove okhttp and logging-interceptor compile dependencies. Preserve debug logging behind RECURLY_INSECURE + RECURLY_DEBUG using System.out.println. Fix 411 errors on POST/PUT with null body by sending Content-Length: 0, matching OkHttp prior behavior. Add contract tests for this case. Update the implementation guide to use OkHttp as the example adapter instead of java.net.http.HttpClient. Co-Authored-By: Claude <noreply@anthropic.com> feat: Add gzip response decompression to DefaultHttpAdapter DefaultHttpAdapter now sets Accept-Encoding: gzip on outgoing requests (unless the caller already set one) and transparently decompresses gzip-encoded response and error bodies via GZIPInputStream, matching the behavior OkHttp provided automatically before the HttpURLConnection migration. content-encoding and content-length are stripped from the returned headers once decompressed since they no longer describe the decompressed body. Co-Authored-By: Claude <noreply@anthropic.com> fix: Exclude WireMock-dependent tests from testCompile on JDK 8 WireMock 3.x ships Java 11 class files, which javac on a JDK 8 toolchain cannot read from the classpath regardless of source/target level. @DisabledOnJre(JRE.JAVA_8) only skips execution, not compilation, so the Java 8 CI job was failing to build. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> fix: Update RequestOptions header tests for HttpAdapter migration Rebase onto v3-v2021-02-25 merged in idempotency-key/custom-header tests written against the old OkHttp-based BaseClient. Rewrite them against the HttpAdapter mock so they compile and assert against the headers map passed to httpAdapter.execute. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 9d33959 commit 14d386b

19 files changed

Lines changed: 828 additions & 230 deletions

docs/http-adapter-implementation-guide.md

Lines changed: 71 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ middleware you prefer. Common reasons to do this:
1111
## Registering your implementation
1212

1313
```java
14-
ClientOptions options = new ClientOptions();
15-
options.setHttpAdapter(new MyHttpAdapter());
14+
ClientOptions options = ClientOptions.builder().httpAdapter(new MyHttpAdapter()).build();
1615
Client client = new Client(apiKey, options);
1716
```
1817

@@ -47,16 +46,16 @@ All headers the client wants to send, including:
4746
| `Content-Type` | `application/json` |
4847
| `User-Agent` | `Recurly/<version>; java <jvm-version>` |
4948

50-
**Forward every entry without modification.** Do not add, remove, or override headers in the
51-
adapter. The client owns header construction; the adapter owns transport.
49+
**Forward every client-supplied entry unmodified.** Do not remove or override a header the client
50+
set. The client owns header construction; the adapter owns transport. You may add your own
51+
transport-layer headers (e.g. `Accept-Encoding`) as long as they don't conflict with a header the
52+
client already set — `DefaultHttpAdapter` does this to negotiate gzip.
5253

5354
### Body
5455

55-
- `POST` and `PUT` requests: a UTF-8-encoded JSON string.
56-
- `GET`, `HEAD`, `DELETE` requests: `null`.
57-
58-
When `body` is `null` and the HTTP method requires a body (e.g. `DELETE` with some servers), send
59-
an empty body (`Content-Length: 0`).
56+
- `POST` and `PUT` requests: typically a UTF-8-encoded JSON string, but may be `null` (e.g. no
57+
request object was passed) — send the request with `Content-Length: 0` in that case.
58+
- `GET`, `HEAD`, `DELETE` requests: always `null`.
6059

6160
---
6261

@@ -77,6 +76,9 @@ status codes.
7776
Pass a `Map<String, String>` of all response headers. `HttpResponse` normalises keys to lower-case
7877
internally, so you do not need to do it yourself — but passing lower-case keys is fine too.
7978

79+
Only one value per header name is supported. When a server sends multiple values for a single
80+
header name (e.g. `Set-Cookie`), pass just the first value.
81+
8082
The client reads these specific headers:
8183

8284
| Header | Purpose |
@@ -159,55 +161,86 @@ public class MyHttpAdapter implements HttpAdapter {
159161
The Recurly client does not enforce timeouts. Set connect, read, and write timeouts inside your
160162
adapter and adjust to your SLA requirements.
161163

164+
`DefaultHttpAdapter` sets its connect and read timeouts from its `timeoutMs` constructor argument
165+
(10 seconds by default). `java.net.HttpURLConnection` has no write-timeout API, so a stalled
166+
request-body upload is not bounded by `timeoutMs` and can block until the underlying OS-level TCP
167+
timeout is reached. If your SLA requires a bounded write phase, implement a custom `HttpAdapter`
168+
(e.g. using OkHttp, which supports `writeTimeout` directly) instead of relying on the default.
169+
162170
---
163171

164-
## Minimal example
172+
## OkHttp example
173+
174+
Add the OkHttp dependency to your project:
175+
176+
```xml
177+
<dependency>
178+
<groupId>com.squareup.okhttp3</groupId>
179+
<artifactId>okhttp</artifactId>
180+
<version>4.12.0</version>
181+
</dependency>
182+
```
165183

166184
```java
167185
import com.recurly.v3.http.HttpAdapter;
168186
import com.recurly.v3.http.HttpResponse;
169187

170188
import java.io.IOException;
171-
import java.net.URI;
172-
import java.net.http.HttpClient;
173-
import java.net.http.HttpRequest;
174-
import java.net.http.HttpRequest.BodyPublishers;
175-
import java.time.Duration;
176189
import java.util.HashMap;
177190
import java.util.Map;
178-
179-
public class JavaNetHttpAdapter implements HttpAdapter {
180-
181-
private final HttpClient client = HttpClient.newBuilder()
182-
.connectTimeout(Duration.ofSeconds(30))
191+
import java.util.concurrent.TimeUnit;
192+
import okhttp3.Headers;
193+
import okhttp3.MediaType;
194+
import okhttp3.OkHttpClient;
195+
import okhttp3.Request;
196+
import okhttp3.RequestBody;
197+
import okhttp3.Response;
198+
import okhttp3.ResponseBody;
199+
200+
public class OkHttpAdapter implements HttpAdapter {
201+
202+
private final OkHttpClient client = new OkHttpClient.Builder()
203+
.connectTimeout(60, TimeUnit.SECONDS)
204+
.readTimeout(60, TimeUnit.SECONDS)
205+
.writeTimeout(60, TimeUnit.SECONDS)
183206
.build();
184207

185208
@Override
186209
public HttpResponse execute(String method, String url,
187210
Map<String, String> headers, String body) throws IOException {
188-
HttpRequest.Builder builder = HttpRequest.newBuilder()
189-
.uri(URI.create(url))
190-
.method(method, body != null
191-
? BodyPublishers.ofString(body)
192-
: BodyPublishers.noBody());
211+
Request.Builder builder = new Request.Builder().url(url);
193212

194-
headers.forEach(builder::header);
213+
for (Map.Entry<String, String> header : headers.entrySet()) {
214+
builder.header(header.getKey(), header.getValue());
215+
}
216+
217+
RequestBody requestBody = body != null
218+
? RequestBody.create(body, MediaType.parse("application/json; charset=utf-8"))
219+
: RequestBody.create(new byte[0], MediaType.parse("application/json; charset=utf-8"));
220+
221+
switch (method) {
222+
case "HEAD": builder.head(); break;
223+
case "GET": builder.get(); break;
224+
case "POST": builder.post(requestBody); break;
225+
case "PUT": builder.put(requestBody); break;
226+
case "DELETE": builder.delete(); break;
227+
default:
228+
throw new IllegalArgumentException(method + " is not a valid Recurly HTTP method");
229+
}
195230

196-
try {
197-
java.net.http.HttpResponse<byte[]> resp =
198-
client.send(builder.build(), java.net.http.HttpResponse.BodyHandlers.ofByteArray());
231+
try (Response response = client.newCall(builder.build()).execute()) {
232+
int statusCode = response.code();
199233

200234
Map<String, String> responseHeaders = new HashMap<>();
201-
resp.headers().map().forEach((k, vs) -> {
202-
if (k != null && !vs.isEmpty()) responseHeaders.put(k, vs.get(0));
203-
});
235+
Headers okHeaders = response.headers();
236+
for (int i = 0; i < okHeaders.size(); i++) {
237+
responseHeaders.put(okHeaders.name(i).toLowerCase(), okHeaders.value(i));
238+
}
204239

205-
return new HttpResponse(resp.statusCode(), responseHeaders,
206-
resp.body() != null ? resp.body() : new byte[0]);
240+
ResponseBody responseBody = response.body();
241+
byte[] responseBodyBytes = responseBody != null ? responseBody.bytes() : new byte[0];
207242

208-
} catch (InterruptedException e) {
209-
Thread.currentThread().interrupt();
210-
throw new IOException("HTTP request interrupted", e);
243+
return new HttpResponse(statusCode, responseHeaders, responseBodyBytes);
211244
}
212245
}
213246
}
@@ -274,4 +307,4 @@ public class FakeHttpAdapter implements HttpAdapter {
274307
}
275308
```
276309

277-
See `DefaultHttpAdapter` for the complete production reference implementation.
310+
See `DefaultHttpAdapter` for the `HttpURLConnection`-based reference implementation.

pom.xml

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,9 @@
5858
</distributionManagement>
5959

6060
<properties>
61-
<java.version>1.8</java.version>
61+
<java.version>8</java.version>
6262
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
6363
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
64-
<okhttp3.version>4.12.0</okhttp3.version>
6564
<wiremock.version>3.13.2</wiremock.version>
6665
<surefire.version>3.5.5</surefire.version>
6766
<jacoco.version>0.8.13</jacoco.version>
@@ -87,8 +86,7 @@
8786
<artifactId>maven-compiler-plugin</artifactId>
8887
<version>3.15.0</version>
8988
<configuration>
90-
<source>${java.version}</source>
91-
<target>${java.version}</target>
89+
<release>${java.version}</release>
9290
</configuration>
9391
</plugin>
9492
<plugin>
@@ -156,6 +154,7 @@
156154
<plugin>
157155
<groupId>org.apache.maven.plugins</groupId>
158156
<artifactId>maven-jar-plugin</artifactId>
157+
<version>3.5.0</version>
159158
<executions>
160159
<execution>
161160
<id>test-jar</id>
@@ -234,22 +233,45 @@
234233
</plugins>
235234
</build>
236235

236+
<profiles>
237+
<profile>
238+
<!--
239+
WireMock 3.x ships class files compiled for Java 11 (class file version 55), which
240+
javac running on a JDK 8 toolchain cannot read from the classpath at all, regardless of
241+
<source>/<target>. HttpAdapterContract and DefaultHttpAdapterGzipTest are already
242+
@DisabledOnJre(JRE.JAVA_8) at runtime, but that annotation only skips execution, not
243+
compilation, so they must be excluded from testCompile on JDK 8 to keep the build green.
244+
-->
245+
<id>jdk8-exclude-wiremock-tests</id>
246+
<activation>
247+
<jdk>1.8</jdk>
248+
</activation>
249+
<build>
250+
<plugins>
251+
<plugin>
252+
<groupId>org.apache.maven.plugins</groupId>
253+
<artifactId>maven-compiler-plugin</artifactId>
254+
<configuration>
255+
<testExcludes>
256+
<exclude>**/http/HttpAdapterContract.java</exclude>
257+
<exclude>**/http/DefaultHttpAdapterContractTest.java</exclude>
258+
<exclude>**/http/DefaultHttpAdapterGzipTest.java</exclude>
259+
<exclude>**/http/DefaultHttpAdapterTimeoutTest.java</exclude>
260+
<exclude>**/http/WireMockTestSupport.java</exclude>
261+
</testExcludes>
262+
</configuration>
263+
</plugin>
264+
</plugins>
265+
</build>
266+
</profile>
267+
</profiles>
268+
237269
<dependencies>
238270
<dependency>
239271
<groupId>com.google.code.gson</groupId>
240272
<artifactId>gson</artifactId>
241273
<version>2.13.1</version>
242274
</dependency>
243-
<dependency>
244-
<groupId>com.squareup.okhttp3</groupId>
245-
<artifactId>okhttp</artifactId>
246-
<version>${okhttp3.version}</version>
247-
</dependency>
248-
<dependency>
249-
<groupId>com.squareup.okhttp3</groupId>
250-
<artifactId>logging-interceptor</artifactId>
251-
<version>${okhttp3.version}</version>
252-
</dependency>
253275
<dependency>
254276
<groupId>org.wiremock</groupId>
255277
<artifactId>wiremock</artifactId>

0 commit comments

Comments
 (0)