From d48c0136bb456385e36a18fc64a456f177c766ec Mon Sep 17 00:00:00 2001 From: Marvin Froeder Date: Thu, 11 Jun 2026 11:35:35 -0300 Subject: [PATCH] revert: move Request.Body streaming breaking changes off master, they belong to 14.x Signed-off-by: Marvin Froeder --- MIGRATION-v14.md | 421 ------------- .../java/feign/error/ExceptionGenerator.java | 4 +- .../AbstractAnnotationErrorDecoderTest.java | 4 +- ...ErrorDecoderExceptionConstructorsTest.java | 7 +- .../benchmark/DecoderIteratorsBenchmark.java | 2 +- core/src/main/java/feign/DefaultClient.java | 9 +- core/src/main/java/feign/DefaultContract.java | 2 +- core/src/main/java/feign/FeignException.java | 6 +- core/src/main/java/feign/Logger.java | 22 +- core/src/main/java/feign/Request.java | 573 +++++++++--------- core/src/main/java/feign/RequestTemplate.java | 60 +- .../main/java/feign/codec/DefaultEncoder.java | 5 +- .../feign/AlwaysEncodeBodyContractTest.java | 15 +- core/src/test/java/feign/AsyncFeignTest.java | 11 +- core/src/test/java/feign/ClientTest.java | 6 +- .../test/java/feign/DefaultContractTest.java | 31 +- .../src/test/java/feign/FeignBuilderTest.java | 3 +- .../test/java/feign/FeignExceptionTest.java | 39 +- core/src/test/java/feign/FeignTest.java | 11 +- .../test/java/feign/FeignUnderAsyncTest.java | 11 +- core/src/test/java/feign/JavaLoggerTest.java | 9 +- .../test/java/feign/LoggerMethodsTest.java | 2 +- .../test/java/feign/LoggerRebufferTest.java | 19 +- .../test/java/feign/RequestTemplateTest.java | 13 +- core/src/test/java/feign/ResponseTest.java | 23 +- .../java/feign/RetryableExceptionTest.java | 6 +- core/src/test/java/feign/RetryerTest.java | 2 +- core/src/test/java/feign/TargetTest.java | 4 +- .../feign/assertj/RequestTemplateAssert.java | 26 +- .../java/feign/codec/DefaultDecoderTest.java | 5 +- .../java/feign/codec/DefaultEncoderTest.java | 17 +- .../DefaultErrorDecoderHttpErrorTest.java | 6 +- .../feign/codec/DefaultErrorDecoderTest.java | 19 +- .../java/feign/stream/StreamDecoderTest.java | 4 +- .../java/feign/metrics4/MeteredEncoder.java | 18 +- .../java/feign/metrics5/MeteredEncoder.java | 18 +- .../feign/fastjson2/Fastjson2Encoder.java | 4 +- .../feign/fastjson2/FastJsonCodecTest.java | 16 +- .../form/MultipartFormContentProcessor.java | 3 +- .../form/UrlencodedFormContentProcessor.java | 3 +- .../feign/form/multipart/DelegateWriter.java | 16 +- .../googlehttpclient/GoogleHttpClient.java | 60 +- .../java/feign/graphql/GraphqlDecoder.java | 11 +- .../graphql/GraphqlRequestInterceptor.java | 2 +- .../feign/graphql/GraphqlDecoderTest.java | 6 +- .../feign/graphql/GraphqlEncoderTest.java | 33 +- .../src/main/java/feign/gson/GsonEncoder.java | 3 +- .../test/java/feign/gson/GsonCodecTest.java | 19 +- .../java/feign/hc5/ApacheHttp5Client.java | 22 +- .../feign/hc5/AsyncApacheHttp5Client.java | 125 +--- .../main/java/feign/hc5/FeignBodyEntity.java | 97 --- .../feign/hc5/AsyncApacheHttp5ClientTest.java | 11 +- .../feign/httpclient/ApacheHttpClient.java | 64 +- .../jackson/jaxb/JacksonJaxbJsonEncoder.java | 4 +- .../jackson/jaxb/JacksonJaxbCodecTest.java | 7 +- .../feign/jackson/jr/JacksonJrEncoder.java | 5 +- .../feign/jackson/jr/JacksonCodecTest.java | 32 +- .../java/feign/jackson/JacksonEncoder.java | 4 +- .../java/feign/jackson/JacksonCodecTest.java | 31 +- .../feign/jackson/JacksonIteratorTest.java | 10 +- .../java/feign/jackson3/Jackson3Encoder.java | 4 +- .../feign/jackson3/Jackson3CodecTest.java | 31 +- .../java/feign/http2client/Http2Client.java | 110 +--- .../test/Http2ClientAsyncTest.java | 11 +- .../src/main/java/feign/jaxb/JAXBEncoder.java | 3 +- .../test/java/feign/jaxb/JAXBCodecTest.java | 27 +- .../jaxb/examples/AWSSignatureVersion4.java | 11 +- .../src/main/java/feign/jaxb/JAXBEncoder.java | 3 +- .../test/java/feign/jaxb/JAXBCodecTest.java | 27 +- .../jaxb/examples/AWSSignatureVersion4.java | 11 +- .../main/java/feign/jaxrs2/JAXRSClient.java | 29 +- .../src/main/java/feign/json/JsonEncoder.java | 3 +- .../test/java/feign/json/JsonCodecTest.java | 7 +- .../test/java/feign/json/JsonDecoderTest.java | 4 +- .../test/java/feign/json/JsonEncoderTest.java | 18 +- .../kotlin/feign/kotlin/CoroutineFeignTest.kt | 5 +- .../java/feign/micrometer/MeteredEncoder.java | 16 +- mock/src/main/java/feign/mock/RequestKey.java | 47 +- .../test/java/feign/mock/MockClientTest.java | 41 +- .../test/java/feign/mock/RequestKeyTest.java | 35 +- .../main/java/feign/moshi/MoshiEncoder.java | 3 +- .../java/feign/moshi/MoshiDecoderTest.java | 19 +- .../main/java/feign/okhttp/OkHttpClient.java | 56 +- .../feign/okhttp/OkHttpClientAsyncTest.java | 11 +- pom.xml | 8 - .../src/main/java/feign/ribbon/LBClient.java | 13 +- .../test/java/feign/ribbon/LBClientTest.java | 4 +- .../test/java/feign/sax/SAXDecoderTest.java | 9 +- .../sax/examples/AWSSignatureVersion4.java | 11 +- .../java/feign/slf4j/Slf4jLoggerTest.java | 11 +- .../src/main/java/feign/soap/SOAPEncoder.java | 9 +- .../test/java/feign/soap/SOAPCodecTest.java | 27 +- .../java/feign/soap/SOAPFaultDecoderTest.java | 13 +- .../src/main/java/feign/soap/SOAPEncoder.java | 3 +- .../test/java/feign/soap/SOAPCodecTest.java | 27 +- .../java/feign/soap/SOAPFaultDecoderTest.java | 13 +- .../java/feign/spring/SpringContractTest.java | 11 +- .../BeanValidationMethodInterceptorTest.java | 4 +- .../BeanValidationMethodInterceptorTest.java | 4 +- .../src/main/java/feign/VertxFeign.java | 11 +- .../java/feign/vertx/OutputToReadStream.java | 308 ---------- .../java/feign/vertx/VertxHttpClient.java | 26 +- .../feign/vertx/ConnectionsLeakTests.java | 2 - .../vertx/Http11ClientReconnectTest.java | 1 - .../java/feign/vertx/QueryMapEncoderTest.java | 1 - .../java/feign/vertx/RawContractTest.java | 1 - .../feign/vertx/RequestPreProcessorTest.java | 1 - .../test/java/feign/vertx/RetryingTest.java | 1 - .../java/feign/vertx/TimeoutHandlingTest.java | 1 - .../java/feign/vertx/VertxHttpClientTest.java | 20 - .../feign/vertx/VertxHttpOptionsTest.java | 2 - .../feign/vertx/ConnectionsLeakTests.java | 2 - .../vertx/Http11ClientReconnectTest.java | 1 - .../java/feign/vertx/QueryMapEncoderTest.java | 1 - .../java/feign/vertx/RawContractTest.java | 1 - .../feign/vertx/RequestPreProcessorTest.java | 1 - .../test/java/feign/vertx/RetryingTest.java | 1 - .../java/feign/vertx/TimeoutHandlingTest.java | 1 - .../java/feign/vertx/VertxHttpClientTest.java | 20 - .../feign/vertx/VertxHttpOptionsTest.java | 2 - 120 files changed, 1037 insertions(+), 2088 deletions(-) delete mode 100644 MIGRATION-v14.md delete mode 100644 hc5/src/main/java/feign/hc5/FeignBodyEntity.java delete mode 100644 vertx/feign-vertx/src/main/java/feign/vertx/OutputToReadStream.java diff --git a/MIGRATION-v14.md b/MIGRATION-v14.md deleted file mode 100644 index e489aa707a..0000000000 --- a/MIGRATION-v14.md +++ /dev/null @@ -1,421 +0,0 @@ -# Migration Guide — Feign v14 (Request Body Streaming) - -This guide covers the breaking changes introduced in #3360 and explains how to update your code. - -> **Target release:** `v14.rc.1` - ---- - -## Overview - -`feign.Request.Body` has been redesigned from a `byte[]`-backed concrete class into a **streaming-ready interface**. -Request bodies are no longer eagerly buffered in memory unless you explicitly use the `byte[]`/`String` factory methods. - -For most users, regular Feign usage via interface annotations and `Feign.builder().target(...)` is unchanged. -The **breaking changes** primarily affect code that interacts directly with request body internals, including: - -- Custom `Encoder` implementations -- Custom `Client` implementations -- Any code that directly reads `Request.body()`, `Request.length()`, `Request.charset()`, or `RequestTemplate.body()`/ - `RequestTemplate.requestBody()` - ---- - -## Breaking Changes - -### 1. `Request.Body` is now an interface - -**Before:** - -```java -// Body was a concrete class with public fields/methods -Request.Body body = Request.Body.create("hello", StandardCharsets.UTF_8); -byte[] bytes = body.asBytes(); -String str = body.asString(); -int len = body.length(); -Optional charset = body.getEncoding(); -boolean binary = body.isBinary(); -``` - -**After:** - -```java -// Body is now an interface — use the factory methods -Request.Body body = Request.Body.of("hello", StandardCharsets.UTF_8); - -// To read content, write it to a stream (note: these methods throw checked IOException): -byte[] bytes = body.writeToByteArray(); -String str = body.writeToString(StandardCharsets.UTF_8); -long len = body.contentLength(); // -1 if unknown/streaming -boolean repeatable = body.isRepeatable(); -``` - -**Removed methods on `Request.Body`:** - -| Removed | Replacement | -|---------------------------------|---------------------------------------------------------------------------------------| -| `Body.create(String)` | `Body.of(String)` | -| `Body.create(String, Charset)` | `Body.of(String, Charset)` | -| `Body.create(byte[])` | `Body.of(byte[])` | -| `Body.create(byte[], Charset)` | `Body.of(byte[], Charset)` | -| `Body.encoded(byte[], Charset)` | `Body.of(byte[], Charset)` | -| `Body.empty()` | Pass `null` for no body | -| `body.asBytes()` | `body.writeToByteArray()` | -| `body.asString()` | `body.writeToString(charset)` | -| `body.length()` → `int` | `body.contentLength()` → `long` (returns `-1` if unknown) | -| `body.getEncoding()` | Read charset from `Content-Type` header | -| `body.isBinary()` | No direct replacement; `body.isRepeatable()` may be useful depending on your use case | - ---- - -### 2. `Request.body()` now returns `Optional` - -**Before:** - -```java -byte[] body = request.body(); // nullable byte[] -if (body != null) { - out.write(body); -} -``` - -**After:** - -```java -// writeTo(OutputStream) throws checked IOException, so a plain Consumer lambda -// (as used in Optional.ifPresent) cannot propagate it. Use an explicit if-block instead. -Optional body = request.body(); -if (body.isPresent()) { - body.get().writeTo(out); -} -``` - ---- - -### 3. `Request.length()` removed - -**Before:** - -```java -int length = request.length(); -``` - -**After:** - -```java -// contentLength() does not throw, so Optional.map is safe here. -long length = request.body() - .map(Request.Body::contentLength) - .orElse(0L); -``` - ---- - -### 4. `Request.charset()` removed - -**Before:** - -```java -Charset charset = request.charset(); -``` - -**After:** Read the charset from the `Content-Type` request header. There is no longer a charset field on `Request` -itself. - ---- - -### 5. `Request.isBinary()` removed - -**Before:** - -```java -boolean binary = request.isBinary(); -``` - -**After:** - -```java -// There is no direct replacement for request.isBinary(). -// Depending on why you were checking it, request body repeatability may be useful: -boolean repeatable = request.body() - .map(Request.Body::isRepeatable) - .orElse(false); -``` - ---- - -### 6. `Request.create(...)` overloads removed - -The `byte[]` + `Charset`-based `Request.create(...)` overloads have been removed. - -**Before:** - -```java -Request.create(HttpMethod.GET, url, headers, bodyBytes, charset); -Request.create(HttpMethod.GET, url, headers, bodyBytes, charset, requestTemplate); -// Deprecated String-based variant: -Request.create("GET", url, headers, bodyBytes, charset); -``` - -**After:** - -```java -// With a body: -Request.create(HttpMethod.GET, url, headers, Request.Body.of(bodyBytes), requestTemplate); - -// Without a body: -Request.create(HttpMethod.GET, url, headers, null, null); -``` - ---- - -### 7. `RequestTemplate` API changes - -#### `RequestTemplate.body(String)` removed - -**Before:** - -```java -template.body("hello world"); -``` - -**After:** - -```java -template.body(Request.Body.of("hello world")); -``` - -#### `RequestTemplate.body(byte[], Charset)` deprecated - -**Before:** - -```java -template.body(bytes, StandardCharsets.UTF_8); -``` - -**After:** - -```java -template.body(Request.Body.of(bytes, StandardCharsets.UTF_8)); -// or, if charset is irrelevant for your use case: -template.body(Request.Body.of(bytes)); -``` - -#### `RequestTemplate.body()` (returns `byte[]`) removed - -**Before:** - -```java -byte[] body = template.body(); -``` - -**After:** - -```java -// writeToByteArray() throws checked IOException, so a plain Function lambda -// (as used in Optional.map) cannot propagate it. Use an explicit if-block instead. -byte[] body = null; -Optional requestBody = template.requestBody(); -if (requestBody.isPresent()) { - body = requestBody.get().writeToByteArray(); -} -``` - -#### `RequestTemplate.requestBody()` is no longer `@Deprecated` - -The method now returns `Optional` and is the primary accessor. - -#### `RequestTemplate.requestCharset()` removed - -**Before:** - -```java -Charset charset = template.requestCharset(); -``` - -**After:** Read charset from the `Content-Type` header. There is no longer a charset tracked on the template itself. - ---- - -### 8. Custom `Encoder` implementations - -If you implement a custom `Encoder`, update calls to `template.body(...)`: - -**Before:** - -```java -template.body(serialized.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); -// or -template.body(serialized); -``` - -**After:** - -```java -template.body(Request.Body.of(serialized, StandardCharsets.UTF_8)); -// or, for UTF-8 strings: -template.body(Request.Body.of(serialized)); -``` - ---- - -### 9. Custom `Client` implementations - -If you implement a custom `Client`, update how you write the request body: - -**Before:** - -```java -byte[] body = request.body(); -if (body != null) { - outputStream.write(body); -} -``` - -**After:** - -```java -// writeTo(OutputStream) throws checked IOException, so a plain Consumer lambda -// (as used in Optional.ifPresent) cannot propagate it. Use an explicit if-block instead. -Optional body = request.body(); -if (body.isPresent()) { - body.get().writeTo(outputStream); -} -``` - -For retry-capable clients, check `body.isRepeatable()` before attempting a retry — non-repeatable (streaming) bodies -cannot be re-sent. - ---- - -### 10. `FeignException.errorReading(...)` — request body no longer captured - -`FeignException` no longer captures the request body when a read error occurs, because the body may be a non-repeatable -stream. Code asserting `exception.contentUTF8()` returns the request body must be updated: - -**Before:** - -```java -assertThat(exception.contentUTF8()).isEqualTo("Request body"); -``` - -**After:** - -```java -assertThat(exception.contentUTF8()).isEmpty(); -``` - ---- - -### 11. `mock` module — `RequestKey` no longer stores `Charset` - -**Before:** - -```java -RequestKey.builder(...).charset(StandardCharsets.UTF_8).build(); -``` - -**After:** The `charset(Charset)` method has been removed from `RequestKey.Builder`. Remove it from your mock setup -code. - ---- - -### 12. `Request.Body` no longer implements `Serializable` - -`feign.Request.Body` previously implemented `java.io.Serializable`. This has been removed. -If you were serializing `Request.Body` objects (e.g., for caching or distributed tracing), you will need an alternative -serialization strategy. - ---- - -### 13. Vert.x integration — `VertxFeign.Builder` now requires `.vertx(Vertx)` - -**Before:** - -```java -VertxFeign.builder() - .webClient(webClient) - .target(MyApi.class, url); -``` - -**After:** - -```java -VertxFeign.builder() - .vertx(vertx) // required — NPE with descriptive message if missing - .webClient(webClient) - .target(MyApi.class, url); -``` - ---- - -## Implementing a Custom Streaming Body - -If you want to stream a body (e.g., from a file or `InputStream`), implement `Request.Body` directly. Because -`writeTo(OutputStream)` itself declares `throws IOException`, the lambda **can** propagate it freely — the restriction -only applies to standard functional interfaces (`Consumer`, `Function`, etc.) that don't -declare checked exceptions. - -```java -// One-shot InputStream — non-repeatable (isRepeatable() defaults to false) -Request.Body streamingBody = outputStream -> { - try (InputStream in = Files.newInputStream(path)) { - byte[] buffer = new byte[8192]; - int read; - while ((read = in.read(buffer)) != -1) { - outputStream.write(buffer, 0, read); - } - // or, with Java 9+: - // in.transferTo(outputStream); - } - // IOException propagates naturally — no try-catch needed here -}; -template.body(streamingBody); -``` - -For a repeatable streaming body (e.g., backed by a file that can be re-read): - -```java -public class FileBody implements Request.Body { - private final Path path; - - public FileBody(Path path) { - this.path = path; - } - - @Override - public void writeTo(OutputStream out) throws IOException { - try (InputStream in = Files.newInputStream(path)) { - byte[] buffer = new byte[8192]; - int read; - while ((read = in.read(buffer)) != -1) { - out.write(buffer, 0, read); - } - // or, with Java 9+: - // in.transferTo(out); - } - } - - @Override - public boolean isRepeatable() { - return true; - } - - @Override - public long contentLength() { - try { - return Files.size(path); - } catch (IOException e) { - return super.contentLength(); // returns -1 - } - } -} -``` - ---- - -## Spring Cloud OpenFeign Compatibility - -`RequestTemplate#body(byte[], Charset)` is kept `@Deprecated` for backward compatibility with -`spring-cloud-openfeign-core`. Spring Cloud OpenFeign users are not required to make any changes immediately, but should -migrate to `body(Request.Body)` once the Spring team provides an updated release. diff --git a/annotation-error-decoder/src/main/java/feign/error/ExceptionGenerator.java b/annotation-error-decoder/src/main/java/feign/error/ExceptionGenerator.java index 23d5f317b4..2d2c0fe74b 100644 --- a/annotation-error-decoder/src/main/java/feign/error/ExceptionGenerator.java +++ b/annotation-error-decoder/src/main/java/feign/error/ExceptionGenerator.java @@ -45,7 +45,9 @@ class ExceptionGenerator { .status(500) .body((Response.Body) null) .headers(testHeaders) - .request(Request.create(Request.HttpMethod.GET, "http://test", testHeaders, null, null)) + .request( + Request.create( + Request.HttpMethod.GET, "http://test", testHeaders, Request.Body.empty(), null)) .build(); } diff --git a/annotation-error-decoder/src/test/java/feign/error/AbstractAnnotationErrorDecoderTest.java b/annotation-error-decoder/src/test/java/feign/error/AbstractAnnotationErrorDecoderTest.java index 1d7375b30e..e66a25a9f4 100644 --- a/annotation-error-decoder/src/test/java/feign/error/AbstractAnnotationErrorDecoderTest.java +++ b/annotation-error-decoder/src/test/java/feign/error/AbstractAnnotationErrorDecoderTest.java @@ -45,7 +45,9 @@ Response testResponse(int status, String body, Map> h .status(status) .body(body, StandardCharsets.UTF_8) .headers(headers) - .request(Request.create(Request.HttpMethod.GET, "http://test", headers, null, null)) + .request( + Request.create( + Request.HttpMethod.GET, "http://test", headers, Request.Body.empty(), null)) .build(); } } diff --git a/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderExceptionConstructorsTest.java b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderExceptionConstructorsTest.java index 82f0d9231c..9728a46f5e 100644 --- a/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderExceptionConstructorsTest.java +++ b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderExceptionConstructorsTest.java @@ -17,6 +17,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import feign.Request; import feign.codec.DefaultDecoder; import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors; import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors.DeclaredDefaultConstructorException; @@ -55,7 +56,11 @@ public class AnnotationErrorDecoderExceptionConstructorsTest private static final String NON_NULL_BODY = "A GIVEN BODY"; private static final feign.Request REQUEST = feign.Request.create( - feign.Request.HttpMethod.GET, "http://test", Collections.emptyMap(), null, null); + feign.Request.HttpMethod.GET, + "http://test", + Collections.emptyMap(), + Request.Body.empty(), + null); private static final feign.Request NO_REQUEST = null; private static final Map> NON_NULL_HEADERS = new HashMap<>(); private static final Map> NO_HEADERS = null; diff --git a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java index ebe9ad7188..76fbc1e1b3 100644 --- a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java +++ b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java @@ -82,7 +82,7 @@ public void buildResponse() { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, null)) + .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(carsJson(Integer.parseInt(size)), Util.UTF_8) .build(); diff --git a/core/src/main/java/feign/DefaultClient.java b/core/src/main/java/feign/DefaultClient.java index ec0df4644b..d8e02fb4ad 100644 --- a/core/src/main/java/feign/DefaultClient.java +++ b/core/src/main/java/feign/DefaultClient.java @@ -32,7 +32,6 @@ import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.TreeMap; import java.util.zip.DeflaterOutputStream; import java.util.zip.GZIPInputStream; @@ -188,9 +187,9 @@ else if (field.equals(ACCEPT_ENCODING)) { connection.addRequestProperty("Accept", "*/*"); } - Optional body = request.body(); + byte[] body = request.body(); - if (body.isPresent()) { + if (body != null) { /* * Ignore disableRequestBuffering flag if the empty body was set, to ensure that internal * retry logic applies to such requests. @@ -210,7 +209,7 @@ else if (field.equals(ACCEPT_ENCODING)) { out = new DeflaterOutputStream(out); } try { - body.get().writeTo(out); + out.write(body); } finally { try { out.close(); @@ -219,7 +218,7 @@ else if (field.equals(ACCEPT_ENCODING)) { } } - if (!body.isPresent() && request.httpMethod().isWithBody()) { + if (body == null && request.httpMethod().isWithBody()) { // To use this Header, set 'sun.net.http.allowRestrictedHeaders' property true. connection.addRequestProperty("Content-Length", "0"); } diff --git a/core/src/main/java/feign/DefaultContract.java b/core/src/main/java/feign/DefaultContract.java index a59b1183c9..79edadde82 100644 --- a/core/src/main/java/feign/DefaultContract.java +++ b/core/src/main/java/feign/DefaultContract.java @@ -76,7 +76,7 @@ public DefaultContract() { "Body annotation was empty on method %s.", data.configKey()); if (body.indexOf('{') == -1) { - data.template().body(Request.Body.of(body)); + data.template().body(body); } else { data.template().bodyTemplate(body); } diff --git a/core/src/main/java/feign/FeignException.java b/core/src/main/java/feign/FeignException.java index f053591c96..b7ea794cae 100644 --- a/core/src/main/java/feign/FeignException.java +++ b/core/src/main/java/feign/FeignException.java @@ -15,9 +15,7 @@ */ package feign; -import static feign.Util.UTF_8; -import static feign.Util.caseInsensitiveCopyOf; -import static feign.Util.checkNotNull; +import static feign.Util.*; import static java.lang.String.format; import static java.util.regex.Pattern.CASE_INSENSITIVE; @@ -184,7 +182,7 @@ static FeignException errorReading(Request request, Response response, IOExcepti format("%s reading %s %s", cause.getMessage(), request.httpMethod(), request.url()), request, cause, - null, + request.body(), request.headers()); } diff --git a/core/src/main/java/feign/Logger.java b/core/src/main/java/feign/Logger.java index ae297d3861..a460251a6a 100644 --- a/core/src/main/java/feign/Logger.java +++ b/core/src/main/java/feign/Logger.java @@ -78,18 +78,16 @@ protected void logRequest(String configKey, Level logLevel, Request request) { } } - long bodyLength = - request - .body() - .map( - body -> { - if (logLevel.ordinal() >= Level.FULL.ordinal()) { - log(configKey, ""); // CRLF - log(configKey, "%s", body.toString()); - } - return body.contentLength(); - }) - .orElse(0L); + int bodyLength = 0; + if (request.body() != null) { + bodyLength = request.length(); + if (logLevel.ordinal() >= Level.FULL.ordinal()) { + String bodyText = + request.charset() != null ? new String(request.body(), request.charset()) : null; + log(configKey, ""); // CRLF + log(configKey, "%s", bodyText != null ? bodyText : "Binary data"); + } + } log(configKey, "---> END HTTP (%s-byte body)", bodyLength); } } diff --git a/core/src/main/java/feign/Request.java b/core/src/main/java/feign/Request.java index 00ff8ad039..a3627a164d 100644 --- a/core/src/main/java/feign/Request.java +++ b/core/src/main/java/feign/Request.java @@ -19,30 +19,150 @@ import static feign.Util.getThreadIdentifier; import static feign.Util.valuesOrEmpty; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; import java.io.Serializable; import java.net.HttpURLConnection; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; /** An immutable request to an http server. */ public final class Request implements Serializable { + + public enum HttpMethod { + GET, + HEAD, + POST(true), + PUT(true), + DELETE, + CONNECT, + OPTIONS, + TRACE, + PATCH(true); + + private final boolean withBody; + + HttpMethod() { + this(false); + } + + HttpMethod(boolean withBody) { + this.withBody = withBody; + } + + public boolean isWithBody() { + return this.withBody; + } + } + + public enum ProtocolVersion { + HTTP_1_0("HTTP/1.0"), + HTTP_1_1("HTTP/1.1"), + HTTP_2("HTTP/2.0"), + MOCK; + + final String protocolVersion; + + ProtocolVersion() { + protocolVersion = name(); + } + + ProtocolVersion(String protocolVersion) { + this.protocolVersion = protocolVersion; + } + + @Override + public String toString() { + return protocolVersion; + } + } + + /** + * No parameters can be null except {@code body} and {@code charset}. All parameters must be + * effectively immutable, via safe copies, not mutating or otherwise. + * + * @deprecated {@link #create(HttpMethod, String, Map, byte[], Charset)} + */ + @Deprecated + public static Request create( + String method, + String url, + Map> headers, + byte[] body, + Charset charset) { + checkNotNull(method, "httpMethod of %s", method); + final HttpMethod httpMethod = HttpMethod.valueOf(method.toUpperCase()); + return create(httpMethod, url, headers, body, charset, null); + } + + /** + * Builds a Request. All parameters must be effectively immutable, via safe copies. + * + * @param httpMethod for the request. + * @param url for the request. + * @param headers to include. + * @param body of the request, can be {@literal null} + * @param charset of the request, can be {@literal null} + * @return a Request + */ + @Deprecated + public static Request create( + HttpMethod httpMethod, + String url, + Map> headers, + byte[] body, + Charset charset) { + return create(httpMethod, url, headers, Body.create(body, charset), null); + } + + /** + * Builds a Request. All parameters must be effectively immutable, via safe copies. + * + * @param httpMethod for the request. + * @param url for the request. + * @param headers to include. + * @param body of the request, can be {@literal null} + * @param charset of the request, can be {@literal null} + * @return a Request + */ + public static Request create( + HttpMethod httpMethod, + String url, + Map> headers, + byte[] body, + Charset charset, + RequestTemplate requestTemplate) { + return create(httpMethod, url, headers, Body.create(body, charset), requestTemplate); + } + + /** + * Builds a Request. All parameters must be effectively immutable, via safe copies. + * + * @param httpMethod for the request. + * @param url for the request. + * @param headers to include. + * @param body of the request, can be {@literal null} + * @return a Request + */ + public static Request create( + HttpMethod httpMethod, + String url, + Map> headers, + Body body, + RequestTemplate requestTemplate) { + return new Request(httpMethod, url, headers, body, requestTemplate); + } + private final HttpMethod httpMethod; private final String url; private final Map> headers; - private final transient Body body; + private final Body body; private final RequestTemplate requestTemplate; private final ProtocolVersion protocolVersion; @@ -70,40 +190,41 @@ public final class Request implements Serializable { } /** - * Builds a Request. All parameters must be effectively immutable, via safe copies. + * Http Method for this request. * - * @param httpMethod for the request. - * @param url for the request. - * @param headers to include. - * @param body of the request, can be {@literal null} - * @return a Request + * @return the HttpMethod string + * @deprecated @see {@link #httpMethod()} */ - public static Request create( - HttpMethod httpMethod, - String url, - Map> headers, - Body body, - RequestTemplate requestTemplate) { - return new Request(httpMethod, url, headers, body, requestTemplate); + @Deprecated + public String method() { + return httpMethod.name(); } /** - * Returns the body of the request, if any. + * Http Method for the request. * - * @return the body of the request, if any + * @return the HttpMethod. */ - public Optional body() { - return Optional.ofNullable(body); + public HttpMethod httpMethod() { + return this.httpMethod; } /** - * Add new entries to request Headers. It overrides existing entries + * URL for the request. * - * @param key - * @param values + * @return URL as a String. */ - public void header(String key, Collection values) { - headers.put(key, values); + public String url() { + return url; + } + + /** + * Request Headers. + * + * @return the request headers. + */ + public Map> headers() { + return Collections.unmodifiableMap(headers); } /** @@ -117,44 +238,54 @@ public void header(String key, String value) { } /** - * Request Headers. + * Add new entries to request Headers. It overrides existing entries * - * @return the request headers. + * @param key + * @param values */ - public Map> headers() { - return Collections.unmodifiableMap(headers); + public void header(String key, Collection values) { + headers.put(key, values); } /** - * Http Method for the request. + * Charset of the request. * - * @return the HttpMethod. + * @return the current character set for the request, may be {@literal null} for binary data. */ - public HttpMethod httpMethod() { - return this.httpMethod; + public Charset charset() { + return body.encoding; } /** - * Request HTTP protocol version + * If present, this is the replayable body to send to the server. In some cases, this may be + * interpretable as text. * - * @return HTTP protocol version + * @see #charset() */ - public ProtocolVersion protocolVersion() { - return protocolVersion; + public byte[] body() { + return body.data; } - @Experimental - public RequestTemplate requestTemplate() { - return this.requestTemplate; + public boolean isBinary() { + return body.isBinary(); } /** - * URL for the request. + * Request Length. * - * @return URL as a String. + * @return size of the request body. */ - public String url() { - return url; + public int length() { + return this.body.length(); + } + + /** + * Request HTTP protocol version + * + * @return HTTP protocol version + */ + public ProtocolVersion protocolVersion() { + return protocolVersion; } /** @@ -178,185 +309,51 @@ public String toString() { } } if (body != null) { - builder.append('\n').append(body); + builder.append('\n').append(body.asString()); } return builder.toString(); } - public enum HttpMethod { - GET, - HEAD, - POST(true), - PUT(true), - DELETE, - CONNECT, - OPTIONS, - TRACE, - PATCH(true); - - private final boolean withBody; - - HttpMethod() { - this(false); - } - - HttpMethod(boolean withBody) { - this.withBody = withBody; - } - - public boolean isWithBody() { - return this.withBody; - } - } - - public enum ProtocolVersion { - HTTP_1_0("HTTP/1.0"), - HTTP_1_1("HTTP/1.1"), - HTTP_2("HTTP/2.0"), - MOCK; - - final String protocolVersion; - - ProtocolVersion() { - protocolVersion = name(); - } - - ProtocolVersion(String protocolVersion) { - this.protocolVersion = protocolVersion; - } - - @Override - public String toString() { - return protocolVersion; - } - } - /** - * Request Body - * - *

Considered experimental, will most likely be made internal going forward. + * Controls the per-request settings currently required to be implemented by all {@link Client + * clients} */ - @Experimental - @FunctionalInterface - public interface Body { - /** - * Creates a new {@link Body} instance from the provided string content. - * - * @param content the string content to be used as the body of the request - * @return a new {@link Body} instance containing the provided string content - * @apiNote It's assumed that the content was constructed using {@link StandardCharsets#UTF_8} - * charset. - */ - static Body of(String content) { - return of(content, StandardCharsets.UTF_8); - } - - /** - * Creates a new {@link Body} instance from the provided byte array. - * - * @param content the byte array representing the body content - * @return a new {@link Body} instance - * @apiNote It's assumed that the byte array can be converted to a string using {@link - * StandardCharsets#UTF_8} charset. - */ - static Body of(byte[] content) { - return of(content, StandardCharsets.UTF_8); - } - - /** - * Creates a new {@link Body} instance from the provided string content, using the specified - * charset. - * - * @param content the string content to be used as the body content - * @param charset the content charset - * @return a new {@link Body} instance containing the provided content - */ - static Body of(String content, Charset charset) { - Objects.requireNonNull(content, "content is required"); - Objects.requireNonNull(charset, "charset is required"); - - return of(content.getBytes(charset), charset); - } - - /** - * Creates a new {@link Body} instance from the provided byte array, using the specified - * charset. - * - * @param content the byte array representing the body content - * @param charset the content charset - * @return a new {@link Body} instance - */ - static Body of(byte[] content, Charset charset) { - return new Request.BodyImpl(content, charset); - } - - /** - * Writes the body content to the provided {@link OutputStream}. - * - * @param outputStream the output stream to which the body content should be written - * @throws IOException if an I/O error occurs while writing the body content - */ - void writeTo(OutputStream outputStream) throws IOException; - - /** - * Writes the body content to a byte array. - * - * @return a byte array containing the body content - * @throws IOException if an I/O error occurs while writing the body content to a byte array - */ - default byte[] writeToByteArray() throws IOException { - try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - writeTo(outputStream); - return outputStream.toByteArray(); - } - } + public static class Options { - /** - * Writes the body content to a string using the specified charset for decoding. - * - * @param charset the charset to be used for decoding the body content - * @return a string representation of the body content - * @throws IOException if an I/O error occurs while writing the body content to a string - */ - default String writeToString(Charset charset) throws IOException { - Objects.requireNonNull(charset, "charset is required"); - return new String(writeToByteArray(), charset); - } + private final long connectTimeout; + private final TimeUnit connectTimeoutUnit; + private final long readTimeout; + private final TimeUnit readTimeoutUnit; + private final boolean followRedirects; + private final Map> threadToMethodOptions; /** - * Returns the content length of the body, or {@code -1} if unknown. This can be used by clients - * to set the {@code Content-Length} header. Defaults to {@code -1}. + * Get an Options by methodName * - * @return the content length, or {@code -1} if unknown + * @param methodName it's your FeignInterface method name. + * @return method Options */ - default long contentLength() { - return -1; + @Experimental + public Options getMethodOptions(String methodName) { + Map methodOptions = + threadToMethodOptions.getOrDefault(getThreadIdentifier(), new HashMap<>()); + return methodOptions.getOrDefault(methodName, this); } /** - * Indicates whether the body can be written multiple times. This is important for clients that - * may need to retry requests, as non-repeatable bodies (e.g., streaming data) cannot be - * re-sent. Defaults to {@code false}. + * Set methodOptions by methodKey and options * - * @return {@code true} if the body can be written multiple times, {@code false} otherwise + * @param methodName it's your FeignInterface method name. + * @param options it's the Options for this method. */ - default boolean isRepeatable() { - return false; + @Experimental + public void setMethodOptions(String methodName, Options options) { + String threadIdentifier = getThreadIdentifier(); + Map methodOptions = + threadToMethodOptions.getOrDefault(threadIdentifier, new HashMap<>()); + threadToMethodOptions.put(threadIdentifier, methodOptions); + methodOptions.put(methodName, options); } - } - - /** - * Controls the per-request settings currently required to be implemented by all {@link Client - * clients} - */ - public static class Options { - - private final long connectTimeout; - private final TimeUnit connectTimeoutUnit; - private final long readTimeout; - private final TimeUnit readTimeoutUnit; - private final boolean followRedirects; - private final Map> threadToMethodOptions; /** * Creates a new Options instance. @@ -441,15 +438,6 @@ public Options() { this(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true); } - /** - * Connect Timeout Value. - * - * @return current timeout value. - */ - public long connectTimeout() { - return connectTimeout; - } - /** * Defaults to 10 seconds. {@code 0} implies no timeout. * @@ -460,52 +448,48 @@ public int connectTimeoutMillis() { } /** - * TimeUnit for the Connection Timeout value. + * Defaults to 60 seconds. {@code 0} implies no timeout. * - * @return TimeUnit + * @see java.net.HttpURLConnection#getReadTimeout() */ - public TimeUnit connectTimeoutUnit() { - return connectTimeoutUnit; + public int readTimeoutMillis() { + return (int) readTimeoutUnit.toMillis(readTimeout); } /** - * Get an Options by methodName + * Defaults to true. {@code false} tells the client to not follow the redirections. * - * @param methodName it's your FeignInterface method name. - * @return method Options + * @see HttpURLConnection#getFollowRedirects() */ - @Experimental - public Options getMethodOptions(String methodName) { - Map methodOptions = - threadToMethodOptions.getOrDefault(getThreadIdentifier(), new HashMap<>()); - return methodOptions.getOrDefault(methodName, this); + public boolean isFollowRedirects() { + return followRedirects; } /** - * Defaults to true. {@code false} tells the client to not follow the redirections. + * Connect Timeout Value. * - * @see HttpURLConnection#getFollowRedirects() + * @return current timeout value. */ - public boolean isFollowRedirects() { - return followRedirects; + public long connectTimeout() { + return connectTimeout; } /** - * Read Timeout value. + * TimeUnit for the Connection Timeout value. * - * @return current read timeout value. + * @return TimeUnit */ - public long readTimeout() { - return readTimeout; + public TimeUnit connectTimeoutUnit() { + return connectTimeoutUnit; } /** - * Defaults to 60 seconds. {@code 0} implies no timeout. + * Read Timeout value. * - * @see java.net.HttpURLConnection#getReadTimeout() + * @return current read timeout value. */ - public int readTimeoutMillis() { - return (int) readTimeoutUnit.toMillis(readTimeout); + public long readTimeout() { + return readTimeout; } /** @@ -516,50 +500,91 @@ public int readTimeoutMillis() { public TimeUnit readTimeoutUnit() { return readTimeoutUnit; } + } - /** - * Set methodOptions by methodKey and options - * - * @param methodName it's your FeignInterface method name. - * @param options it's the Options for this method. - */ - @Experimental - public void setMethodOptions(String methodName, Options options) { - String threadIdentifier = getThreadIdentifier(); - Map methodOptions = - threadToMethodOptions.getOrDefault(threadIdentifier, new HashMap<>()); - threadToMethodOptions.put(threadIdentifier, methodOptions); - methodOptions.put(methodName, options); - } + @Experimental + public RequestTemplate requestTemplate() { + return this.requestTemplate; } - private static class BodyImpl implements Body { - private final byte[] content; - private final Charset charset; + /** + * Request Body + * + *

Considered experimental, will most likely be made internal going forward. + */ + @Experimental + public static class Body implements Serializable { + + private transient Charset encoding; + + private byte[] data; - private BodyImpl(byte[] content, Charset charset) { - this.content = Objects.requireNonNull(content, "content must not be null"); - this.charset = Objects.requireNonNull(charset, "charset must not be null"); + private Body() { + super(); } - @Override - public void writeTo(OutputStream outputStream) throws IOException { - Objects.requireNonNull(outputStream, "outputStream is required").write(content); + private Body(byte[] data) { + this.data = data; } - @Override - public long contentLength() { - return content.length; + private Body(byte[] data, Charset encoding) { + this.data = data; + this.encoding = encoding; } - @Override - public boolean isRepeatable() { - return true; + public Optional getEncoding() { + return Optional.ofNullable(this.encoding); } - @Override - public String toString() { - return new String(content, charset); + public int length() { + /* calculate the content length based on the data provided */ + return data != null ? data.length : 0; + } + + public byte[] asBytes() { + return data; + } + + public String asString() { + return !isBinary() ? new String(data, encoding) : "Binary data"; + } + + public boolean isBinary() { + return encoding == null || data == null; + } + + public static Body create(String data) { + return new Body(data.getBytes()); + } + + public static Body create(String data, Charset charset) { + return new Body(data.getBytes(charset), charset); + } + + public static Body create(byte[] data) { + return new Body(data); + } + + public static Body create(byte[] data, Charset charset) { + return new Body(data, charset); + } + + /** + * Creates a new Request Body with charset encoded data. + * + * @param data to be encoded. + * @param charset to encode the data with. if {@literal null}, then data will be considered + * binary and will not be encoded. + * @return a new Request.Body instance with the encoded data. + * @deprecated please use {@link Request.Body#create(byte[], Charset)} + */ + @Deprecated + public static Body encoded(byte[] data, Charset charset) { + return create(data, charset); + } + + public static Body empty() { + return new Body(); } } } diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java index b4868481fb..6f376c4ae1 100644 --- a/core/src/main/java/feign/RequestTemplate.java +++ b/core/src/main/java/feign/RequestTemplate.java @@ -38,7 +38,6 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Optional; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -63,7 +62,7 @@ public final class RequestTemplate implements Serializable { private BodyTemplate bodyTemplate; private HttpMethod method; private transient Charset charset = Util.UTF_8; - private transient Request.Body body; + private Request.Body body = Request.Body.empty(); private boolean decodeSlash = true; private CollectionFormat collectionFormat = CollectionFormat.EXPLODED; private MethodMetadata methodMetadata; @@ -254,7 +253,7 @@ public RequestTemplate resolve(Map variables) { } if (this.bodyTemplate != null) { - resolved.body(Request.Body.of(this.bodyTemplate.expand(variables))); + resolved.body(this.bodyTemplate.expand(variables)); } /* mark the new template resolved */ @@ -882,14 +881,22 @@ public Map> headers() { * Sets the Body and Charset for this request. * * @param data to send, can be null. - * @param ignoredCharset of the encoded data. + * @param charset of the encoded data. * @return a RequestTemplate for chaining. - * @deprecated this method is kept to maintain compatibility with {@code - * spring-cloud-openfeign-core}. Please use {@link #body(Request.Body)} instead. */ - @Deprecated - public RequestTemplate body(byte[] data, Charset ignoredCharset) { - this.body(Request.Body.of(data)); + public RequestTemplate body(byte[] data, Charset charset) { + this.body(Request.Body.create(data, charset)); + return this; + } + + /** + * Set the Body for this request. Charset is assumed to be UTF_8. Data must be encoded. + * + * @param bodyText to send. + * @return a RequestTemplate for chaining. + */ + public RequestTemplate body(String bodyText) { + this.body(Request.Body.create(bodyText.getBytes(this.charset), this.charset)); return this; } @@ -898,30 +905,53 @@ public RequestTemplate body(byte[] data, Charset ignoredCharset) { * * @param body to send. * @return a RequestTemplate for chaining. + * @deprecated use {@link #body(byte[], Charset)} instead. */ + @Deprecated public RequestTemplate body(Request.Body body) { this.body = body; /* body template must be cleared to prevent double processing */ this.bodyTemplate = null; - /* reset any prior Content-Length so it cannot be duplicated or left stale */ - this.header(CONTENT_LENGTH, Collections.emptyList()); - if (body.contentLength() >= 0) { - this.header(CONTENT_LENGTH, String.valueOf(body.contentLength())); + header(CONTENT_LENGTH, Collections.emptyList()); + if (body.length() > 0) { + header(CONTENT_LENGTH, String.valueOf(body.length())); } return this; } + /** + * Charset of the Request Body, if known. + * + * @return the currently applied Charset. + */ + public Charset requestCharset() { + if (this.body != null) { + return this.body.getEncoding().orElse(this.charset); + } + return this.charset; + } + + /** + * The Request Body. + * + * @return the request body. + */ + public byte[] body() { + return body.asBytes(); + } + /** * The Request.Body internal object. * * @return the internal Request.Body. * @deprecated this abstraction is leaky and will be removed in later releases. */ - public Optional requestBody() { - return Optional.ofNullable(this.body); + @Deprecated + public Request.Body requestBody() { + return this.body; } /** diff --git a/core/src/main/java/feign/codec/DefaultEncoder.java b/core/src/main/java/feign/codec/DefaultEncoder.java index a32cf2065a..39a0f8daba 100644 --- a/core/src/main/java/feign/codec/DefaultEncoder.java +++ b/core/src/main/java/feign/codec/DefaultEncoder.java @@ -17,7 +17,6 @@ import static java.lang.String.format; -import feign.Request; import feign.RequestTemplate; import java.lang.reflect.Type; @@ -26,9 +25,9 @@ public class DefaultEncoder implements Encoder { @Override public void encode(Object object, Type bodyType, RequestTemplate template) { if (bodyType == String.class) { - template.body(Request.Body.of(object.toString())); + template.body(object.toString()); } else if (bodyType == byte[].class) { - template.body(Request.Body.of((byte[]) object)); + template.body((byte[]) object, null); } else if (object != null) { throw new EncodeException( format("%s is not a type supported by this encoder.", object.getClass())); diff --git a/core/src/test/java/feign/AlwaysEncodeBodyContractTest.java b/core/src/test/java/feign/AlwaysEncodeBodyContractTest.java index a3100d882d..7a626d6ef0 100644 --- a/core/src/test/java/feign/AlwaysEncodeBodyContractTest.java +++ b/core/src/test/java/feign/AlwaysEncodeBodyContractTest.java @@ -17,7 +17,6 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import feign.codec.EncodeException; import feign.codec.Encoder; @@ -66,7 +65,7 @@ public void encode(Object object, Type bodyType, RequestTemplate template) Object[] methodParameters = (Object[]) object; String body = Arrays.stream(methodParameters).map(String::valueOf).collect(Collectors.joining()); - template.body(Request.Body.of(body)); + template.body(body); } } @@ -74,22 +73,14 @@ private static class BodyParameterSampleEncoder implements Encoder { @Override public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { - template.body(Request.Body.of(String.valueOf(object))); + template.body(String.valueOf(object)); } } private static class SampleClient implements Client { @Override public Response execute(Request request, Request.Options options) throws IOException { - return Response.builder() - .status(200) - .request(request) - .body(request.body().map(this::bodyAsBytes).orElse(null)) - .build(); - } - - private byte[] bodyAsBytes(Request.Body body) { - return assertDoesNotThrow(body::writeToByteArray); + return Response.builder().status(200).request(request).body(request.body()).build(); } } diff --git a/core/src/test/java/feign/AsyncFeignTest.java b/core/src/test/java/feign/AsyncFeignTest.java index 2e683de5f8..300385be64 100644 --- a/core/src/test/java/feign/AsyncFeignTest.java +++ b/core/src/test/java/feign/AsyncFeignTest.java @@ -18,6 +18,7 @@ import static feign.ExceptionPropagationPolicy.UNWRAP; import static feign.Util.UTF_8; import static feign.assertj.MockWebServerAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; import static org.assertj.core.data.MapEntry.entry; @@ -620,7 +621,7 @@ void throwsFeignExceptionIncludingBody() throws Throwable { } catch (FeignException e) { assertThat(e.getMessage()) .contains("timeout reading POST http://localhost:" + server.getPort() + "/"); - assertThat(e.contentUTF8()).isEmpty(); + assertThat(e.contentUTF8()).isEqualTo("Request body"); return; } fail(""); @@ -735,7 +736,7 @@ void whenReturnTypeIsResponseNoErrorHandling() throws Throwable { .status(302) .reason("Found") .headers(headers) - .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, null)) + .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, Util.UTF_8)) .body(new byte[0]) .build(); @@ -1006,7 +1007,7 @@ private Response responseWithText(String text) { return Response.builder() .body(text, Util.UTF_8) .status(200) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(new HashMap<>()) .build(); } @@ -1263,9 +1264,9 @@ static final class TestInterfaceAsyncBuilder { .encoder( (object, _, template) -> { if (object instanceof Map) { - template.body(Request.Body.of(new Gson().toJson(object))); + template.body(new Gson().toJson(object)); } else { - template.body(Request.Body.of(object.toString())); + template.body(object.toString()); } }); diff --git a/core/src/test/java/feign/ClientTest.java b/core/src/test/java/feign/ClientTest.java index c094cadabf..cdfe54d292 100644 --- a/core/src/test/java/feign/ClientTest.java +++ b/core/src/test/java/feign/ClientTest.java @@ -61,12 +61,13 @@ void testConvertAndSendWithAcceptEncoding() throws IOException { headers.put(Util.ACCEPT_ENCODING, acceptEncoding); RequestTemplate requestTemplate = mock(RequestTemplate.class); + Request.Body body = mock(Request.Body.class); Request.Options options = mock(Request.Options.class); Client client = mock(Client.class); Request request = Request.create( - Request.HttpMethod.GET, "http://example.com", headers, null, requestTemplate); + Request.HttpMethod.GET, "http://example.com", headers, body, requestTemplate); DefaultClient defaultClient = new DefaultClient(null, null); HttpURLConnection urlConnection = defaultClient.convertAndSend(request, options); Map> requestProperties = urlConnection.getRequestProperties(); @@ -81,12 +82,13 @@ void testConvertAndSendWithContentLength() throws IOException { headers.put(Util.CONTENT_LENGTH, Collections.singletonList("100")); RequestTemplate requestTemplate = mock(RequestTemplate.class); + Request.Body body = mock(Request.Body.class); Request.Options options = mock(Request.Options.class); Client client = mock(Client.class); Request request = Request.create( - Request.HttpMethod.GET, "http://example.com", headers, null, requestTemplate); + Request.HttpMethod.GET, "http://example.com", headers, body, requestTemplate); DefaultClient defaultClient = new DefaultClient(null, null); HttpURLConnection urlConnection = defaultClient.convertAndSend(request, options); Map> requestProperties = urlConnection.getRequestProperties(); diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java index 0ecf46f4ae..6b26d1ed0e 100644 --- a/core/src/test/java/feign/DefaultContractTest.java +++ b/core/src/test/java/feign/DefaultContractTest.java @@ -17,6 +17,7 @@ import static feign.assertj.FeignAssertions.assertThat; import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.data.MapEntry.entry; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -144,15 +145,7 @@ void headersOnMethodAddsContentTypeHeader() throws Exception { assertThat(md.template()) .hasHeaders( entry("Content-Type", asList("application/xml")), - entry( - "Content-Length", - asList( - md.template() - .requestBody() - .map(Request.Body::contentLength) - .filter(contentLength -> contentLength >= 0) - .map(String::valueOf) - .orElse("0")))); + entry("Content-Length", asList(String.valueOf(md.template().body().length)))); } @Test @@ -162,15 +155,7 @@ void headersOnTypeAddsContentTypeHeader() throws Exception { assertThat(md.template()) .hasHeaders( entry("Content-Type", asList("application/xml")), - entry( - "Content-Length", - asList( - md.template() - .requestBody() - .map(Request.Body::contentLength) - .filter(contentLength -> contentLength >= 0) - .map(String::valueOf) - .orElse("0")))); + entry("Content-Length", asList(String.valueOf(md.template().body().length)))); } @Test @@ -180,15 +165,7 @@ void headersContainsWhitespaces() throws Exception { assertThat(md.template()) .hasHeaders( entry("Content-Type", Collections.singletonList("application/xml")), - entry( - "Content-Length", - asList( - md.template() - .requestBody() - .map(Request.Body::contentLength) - .filter(contentLength -> contentLength >= 0) - .map(String::valueOf) - .orElse("0")))); + entry("Content-Length", asList(String.valueOf(md.template().body().length)))); } @Test diff --git a/core/src/test/java/feign/FeignBuilderTest.java b/core/src/test/java/feign/FeignBuilderTest.java index d9ff04604d..2db2fd9df5 100644 --- a/core/src/test/java/feign/FeignBuilderTest.java +++ b/core/src/test/java/feign/FeignBuilderTest.java @@ -16,6 +16,7 @@ package feign; import static feign.assertj.MockWebServerAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; @@ -209,7 +210,7 @@ void overrideEncoder() throws Exception { server.enqueue(new MockResponse().setBody("response data")); String url = "http://localhost:" + server.getPort(); - Encoder encoder = (object, _, template) -> template.body(Request.Body.of(object.toString())); + Encoder encoder = (object, _, template) -> template.body(object.toString()); TestInterface api = Feign.builder().encoder(encoder).target(TestInterface.class, url); api.encodedPost(Arrays.asList("This", "is", "my", "request")); diff --git a/core/src/test/java/feign/FeignExceptionTest.java b/core/src/test/java/feign/FeignExceptionTest.java index 660af75fcf..f86d35fe19 100644 --- a/core/src/test/java/feign/FeignExceptionTest.java +++ b/core/src/test/java/feign/FeignExceptionTest.java @@ -39,7 +39,8 @@ void canCreateWithRequestAndResponse() { Request.HttpMethod.GET, "/home", Collections.emptyMap(), - Request.Body.of("data".getBytes(StandardCharsets.UTF_8)), + "data".getBytes(StandardCharsets.UTF_8), + StandardCharsets.UTF_8, null); Response response = @@ -51,7 +52,7 @@ void canCreateWithRequestAndResponse() { FeignException exception = FeignException.errorReading(request, response, new IOException("socket closed")); - assertThat(exception.responseBody()).isEmpty(); + assertThat(exception.responseBody()).isNotEmpty(); assertThat(exception.hasRequest()).isTrue(); assertThat(exception.request()).isNotNull(); } @@ -63,12 +64,14 @@ void canCreateWithRequestOnly() { Request.HttpMethod.GET, "/home", Collections.emptyMap(), - Request.Body.of("data".getBytes(StandardCharsets.UTF_8)), + "data".getBytes(StandardCharsets.UTF_8), + StandardCharsets.UTF_8, null); FeignException exception = FeignException.errorExecuting(request, new IOException("connection timeout")); assertThat(exception.responseBody()).isEmpty(); + assertThat(exception.content()).isNullOrEmpty(); assertThat(exception.hasRequest()).isTrue(); assertThat(exception.request()).isNotNull(); } @@ -87,7 +90,8 @@ void createFeignExceptionWithCorrectCharsetResponse() { Request.HttpMethod.GET, "/home", Collections.emptyMap(), - Request.Body.of("data".getBytes(StandardCharsets.UTF_16BE)), + "data".getBytes(StandardCharsets.UTF_16BE), + StandardCharsets.UTF_16BE, null); Response response = @@ -123,7 +127,8 @@ void createFeignExceptionWithCorrectCharsetResponseButDifferentContentTypeFormat Request.HttpMethod.GET, "/home", Collections.emptyMap(), - Request.Body.of("data".getBytes(StandardCharsets.UTF_16BE)), + "data".getBytes(StandardCharsets.UTF_16BE), + StandardCharsets.UTF_16BE, null); Response response = @@ -153,7 +158,8 @@ void createFeignExceptionWithErrorCharsetResponse() { Request.HttpMethod.GET, "/home", Collections.emptyMap(), - Request.Body.of("data".getBytes(StandardCharsets.UTF_16BE)), + "data".getBytes(StandardCharsets.UTF_16BE), + StandardCharsets.UTF_16BE, null); Response response = @@ -176,7 +182,8 @@ void canGetResponseHeadersFromException() { Request.HttpMethod.GET, "/home", Collections.emptyMap(), - Request.Body.of("data".getBytes(StandardCharsets.UTF_8)), + "data".getBytes(StandardCharsets.UTF_8), + StandardCharsets.UTF_8, null); Map> responseHeaders = new HashMap<>(); @@ -234,7 +241,8 @@ void lengthOfBodyExceptionTest() { Request.HttpMethod.GET, "/home", Collections.emptyMap(), - Request.Body.of("data".getBytes(StandardCharsets.UTF_8)), + "data".getBytes(StandardCharsets.UTF_8), + StandardCharsets.UTF_8, null); Response response = @@ -265,12 +273,7 @@ void nullRequestShouldThrowNPEwThrowableAndBytes() { NullPointerException.class, () -> new Derived( - 404, - "message", - null, - new Throwable(), - "content".getBytes(StandardCharsets.UTF_8), - Collections.emptyMap())); + 404, "message", null, new Throwable(), new byte[1], Collections.emptyMap())); } @Test @@ -282,13 +285,7 @@ void nullRequestShouldThrowNPE() { void nullRequestShouldThrowNPEwBytes() { assertThrows( NullPointerException.class, - () -> - new Derived( - 404, - "message", - null, - "content".getBytes(StandardCharsets.UTF_8), - Collections.emptyMap())); + () -> new Derived(404, "message", null, new byte[1], Collections.emptyMap())); } static class Derived extends FeignException { diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 41b3e0e331..47a348bfaf 100755 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -19,6 +19,7 @@ import static feign.Util.UTF_8; import static feign.assertj.MockWebServerAssertions.assertThat; import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; import static org.assertj.core.data.MapEntry.entry; @@ -606,7 +607,7 @@ void throwsFeignExceptionIncludingBody() { } catch (FeignException e) { assertThat(e.getMessage()) .isEqualTo("timeout reading POST http://localhost:" + server.getPort() + "/"); - assertThat(e.contentUTF8()).isEmpty(); + assertThat(e.contentUTF8()).isEqualTo("Request body"); } } @@ -771,7 +772,7 @@ void whenReturnTypeIsResponseNoErrorHandling() { .status(302) .reason("Found") .headers(headers) - .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, null)) + .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, Util.UTF_8)) .body(new byte[0]) .build(); @@ -961,7 +962,7 @@ private Response responseWithText(String text) { return Response.builder() .body(text, Util.UTF_8) .status(200) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(new HashMap<>()) .build(); } @@ -1491,9 +1492,9 @@ static final class TestInterfaceBuilder { .encoder( (object, _, template) -> { if (object instanceof Map) { - template.body(Request.Body.of(new Gson().toJson(object))); + template.body(new Gson().toJson(object)); } else { - template.body(Request.Body.of(object.toString())); + template.body(object.toString()); } }); diff --git a/core/src/test/java/feign/FeignUnderAsyncTest.java b/core/src/test/java/feign/FeignUnderAsyncTest.java index 0440fad8a6..f18fd5d747 100644 --- a/core/src/test/java/feign/FeignUnderAsyncTest.java +++ b/core/src/test/java/feign/FeignUnderAsyncTest.java @@ -17,6 +17,7 @@ import static feign.Util.UTF_8; import static feign.assertj.MockWebServerAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; import static org.assertj.core.data.MapEntry.entry; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -492,7 +493,7 @@ void throwsFeignExceptionIncludingBody() { } catch (FeignException e) { assertThat(e.getMessage()) .isEqualTo("timeout reading POST http://localhost:" + server.getPort() + "/"); - assertThat(e.contentUTF8()).isEmpty(); + assertThat(e.contentUTF8()).isEqualTo("Request body"); } } @@ -526,7 +527,7 @@ void whenReturnTypeIsResponseNoErrorHandling() { .status(302) .reason("Found") .headers(headers) - .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, null)) + .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, Util.UTF_8)) .body(new byte[0]) .build(); @@ -721,7 +722,7 @@ private Response responseWithText(String text) { return Response.builder() .body(text, Util.UTF_8) .status(200) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(new HashMap<>()) .build(); } @@ -963,9 +964,9 @@ static final class TestInterfaceBuilder { .encoder( (object, _, template) -> { if (object instanceof Map) { - template.body(Request.Body.of(new Gson().toJson(object))); + template.body(new Gson().toJson(object)); } else { - template.body(Request.Body.of(object.toString())); + template.body(object.toString()); } }); diff --git a/core/src/test/java/feign/JavaLoggerTest.java b/core/src/test/java/feign/JavaLoggerTest.java index 3799d79b4b..f05d839e15 100644 --- a/core/src/test/java/feign/JavaLoggerTest.java +++ b/core/src/test/java/feign/JavaLoggerTest.java @@ -43,7 +43,8 @@ void rebuffersResponseBodyWhenJulLevelIsInfo() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body("{\"error\":\"test\"}", Util.UTF_8) .build(); @@ -68,7 +69,8 @@ void rebuffersResponseBodyWhenJulLevelIsWarning() throws Exception { Response.builder() .status(500) .reason("Internal Server Error") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body("{\"message\":\"error details\"}", Util.UTF_8) .build(); @@ -93,7 +95,8 @@ void responseBodyReadableMultipleTimesForErrorDecoder() throws Exception { .status(400) .reason("Bad Request") .request( - Request.create(HttpMethod.POST, "/api/submit", Collections.emptyMap(), null, null)) + Request.create( + HttpMethod.POST, "/api/submit", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body(originalBody, Util.UTF_8) .build(); diff --git a/core/src/test/java/feign/LoggerMethodsTest.java b/core/src/test/java/feign/LoggerMethodsTest.java index 36c8c6a913..bee76be63b 100644 --- a/core/src/test/java/feign/LoggerMethodsTest.java +++ b/core/src/test/java/feign/LoggerMethodsTest.java @@ -36,7 +36,7 @@ protected void log(String configKey, String format, Object... args) {} @Test void responseIsClosedAfterRebuffer() throws IOException { Request request = - Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null); + Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, UTF_8, null); Response response = Response.builder() .status(200) diff --git a/core/src/test/java/feign/LoggerRebufferTest.java b/core/src/test/java/feign/LoggerRebufferTest.java index ab1e1f2309..5928a8f255 100644 --- a/core/src/test/java/feign/LoggerRebufferTest.java +++ b/core/src/test/java/feign/LoggerRebufferTest.java @@ -43,7 +43,8 @@ void headersLevelRebuffersResponseBody() throws Exception { .status(404) .reason("Not Found") .request( - Request.create(HttpMethod.GET, "/api/resource", Collections.emptyMap(), null, null)) + Request.create( + HttpMethod.GET, "/api/resource", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body(originalBody, Util.UTF_8) .build(); @@ -69,7 +70,8 @@ void basicLevelDoesNotRebufferResponseBody() throws Exception { .status(200) .reason("OK") .request( - Request.create(HttpMethod.GET, "/api/resource", Collections.emptyMap(), null, null)) + Request.create( + HttpMethod.GET, "/api/resource", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body(originalBody, Util.UTF_8) .build(); @@ -92,7 +94,8 @@ void fullLevelRebuffersResponseBody() throws Exception { .status(200) .reason("OK") .request( - Request.create(HttpMethod.POST, "/api/create", Collections.emptyMap(), null, null)) + Request.create( + HttpMethod.POST, "/api/create", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body(originalBody, Util.UTF_8) .build(); @@ -119,7 +122,8 @@ void noneLevelDoesNotRebufferResponseBody() throws Exception { .status(200) .reason("OK") .request( - Request.create(HttpMethod.GET, "/api/status", Collections.emptyMap(), null, null)) + Request.create( + HttpMethod.GET, "/api/status", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body(originalBody, Util.UTF_8) .build(); @@ -142,7 +146,7 @@ void http204DoesNotRebufferEvenAtHeadersLevel() throws Exception { .reason("No Content") .request( Request.create( - HttpMethod.DELETE, "/api/resource/1", Collections.emptyMap(), null, null)) + HttpMethod.DELETE, "/api/resource/1", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body("should be ignored", Util.UTF_8) .build(); @@ -164,7 +168,8 @@ void http205DoesNotRebufferEvenAtHeadersLevel() throws Exception { .status(205) .reason("Reset Content") .request( - Request.create(HttpMethod.POST, "/api/form", Collections.emptyMap(), null, null)) + Request.create( + HttpMethod.POST, "/api/form", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body("should be ignored", Util.UTF_8) .build(); @@ -187,7 +192,7 @@ void nullBodyHandledCorrectlyAtHeadersLevel() throws Exception { .reason("OK") .request( Request.create( - HttpMethod.HEAD, "/api/resource", Collections.emptyMap(), null, null)) + HttpMethod.HEAD, "/api/resource", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body((byte[]) null) .build(); diff --git a/core/src/test/java/feign/RequestTemplateTest.java b/core/src/test/java/feign/RequestTemplateTest.java index 16106c9277..d66b8db289 100644 --- a/core/src/test/java/feign/RequestTemplateTest.java +++ b/core/src/test/java/feign/RequestTemplateTest.java @@ -17,6 +17,7 @@ import static feign.assertj.FeignAssertions.assertThat; import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.data.MapEntry.entry; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -91,9 +92,11 @@ void resolveTemplateWithParameterizedPathSkipsEncodingSlash() { @Test void resolveTemplateWithBinaryBody() { - byte[] body = new byte[] {7, 3, -3, -7}; RequestTemplate template = - new RequestTemplate().method(HttpMethod.GET).uri("{zoneId}").body(Request.Body.of(body)); + new RequestTemplate() + .method(HttpMethod.GET) + .uri("{zoneId}") + .body(new byte[] {7, 3, -3, -7}, null); template = template.resolve(mapOf("zoneId", "/hostedzone/Z1PA6795UKMFR9")); assertThat(template).hasUrl("/hostedzone/Z1PA6795UKMFR9"); @@ -342,11 +345,7 @@ void resolveTemplateWithBodyTemplateSetsBodyAndContentLength() { .hasHeaders( entry( "Content-Length", - Collections.singletonList( - template - .requestBody() - .map(body -> String.valueOf(body.contentLength())) - .orElse("0")))); + Collections.singletonList(String.valueOf(template.body().length)))); } @Test diff --git a/core/src/test/java/feign/ResponseTest.java b/core/src/test/java/feign/ResponseTest.java index 5a7020f0ff..9af1ceb887 100644 --- a/core/src/test/java/feign/ResponseTest.java +++ b/core/src/test/java/feign/ResponseTest.java @@ -36,7 +36,8 @@ void reasonPhraseIsOptional() { Response.builder() .status(200) .headers(Collections.>emptyMap()) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .body(new byte[0]) .build(); @@ -53,7 +54,8 @@ void canAccessHeadersCaseInsensitively() { Response.builder() .status(200) .headers(headersMap) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .body(new byte[0]) .build(); assertThat(response.headers()) @@ -78,7 +80,8 @@ void charsetSupportsMediaTypesWithQuotedCharset() { Response.builder() .status(200) .headers(headersMap) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .body(new byte[0]) .build(); assertThat(response.charset()).isEqualTo(Util.UTF_8); @@ -94,7 +97,8 @@ void headerValuesWithSameNameOnlyVaryingInCaseAreMerged() { Response.builder() .status(200) .headers(headersMap) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .body(new byte[0]) .build(); @@ -111,7 +115,8 @@ void headersAreOptional() { Response response = Response.builder() .status(200) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .body(new byte[0]) .build(); assertThat(response.headers()).isNotNull().isEmpty(); @@ -122,7 +127,8 @@ void support1xxStatusCodes() { Response response = Response.builder() .status(103) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .body((Response.Body) null) .build(); @@ -139,7 +145,7 @@ void statusCodesOfAnyValueAreAllowed() { .status(statusCode) .request( Request.create( - HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .body((Response.Body) null) .build(); @@ -152,7 +158,8 @@ void protocolVersionDefaultsToHttp1_1() { Response response = Response.builder() .status(200) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .protocolVersion(null) .body(new byte[0]) .build(); diff --git a/core/src/test/java/feign/RetryableExceptionTest.java b/core/src/test/java/feign/RetryableExceptionTest.java index 90fc028eb9..0bd9a73127 100644 --- a/core/src/test/java/feign/RetryableExceptionTest.java +++ b/core/src/test/java/feign/RetryableExceptionTest.java @@ -33,7 +33,7 @@ void createRetryableExceptionWithResponseAndResponseHeader() { // given Long retryAfter = 5000L; Request request = - Request.create(Request.HttpMethod.GET, "/", Collections.emptyMap(), null, null); + Request.create(Request.HttpMethod.GET, "/", Collections.emptyMap(), null, Util.UTF_8); byte[] response = "response".getBytes(StandardCharsets.UTF_8); Map> responseHeader = new HashMap<>(); responseHeader.put("TEST_HEADER", Arrays.asList("TEST_CONTENT")); @@ -56,7 +56,7 @@ void createRetryableExceptionWithMethodKey() { Long retryAfter = 5000L; String methodKey = "TestClient#testMethod()"; Request request = - Request.create(Request.HttpMethod.GET, "/", Collections.emptyMap(), null, null); + Request.create(Request.HttpMethod.GET, "/", Collections.emptyMap(), null, Util.UTF_8); Throwable cause = new RuntimeException("test cause"); // when @@ -81,7 +81,7 @@ void createRetryableExceptionWithMethodKey() { void methodKeyIsNullWhenNotProvided() { // given Request request = - Request.create(Request.HttpMethod.GET, "/", Collections.emptyMap(), null, null); + Request.create(Request.HttpMethod.GET, "/", Collections.emptyMap(), null, Util.UTF_8); // when RetryableException retryableException = diff --git a/core/src/test/java/feign/RetryerTest.java b/core/src/test/java/feign/RetryerTest.java index d2656b8686..74ed8e832b 100644 --- a/core/src/test/java/feign/RetryerTest.java +++ b/core/src/test/java/feign/RetryerTest.java @@ -26,7 +26,7 @@ class RetryerTest { private static final Request REQUEST = - Request.create(Request.HttpMethod.GET, "/", Collections.emptyMap(), null, null); + Request.create(Request.HttpMethod.GET, "/", Collections.emptyMap(), null, Util.UTF_8); @Test void only5TriesAllowedAndExponentialBackoff() { diff --git a/core/src/test/java/feign/TargetTest.java b/core/src/test/java/feign/TargetTest.java index 73e9fb5e92..024a9514f5 100644 --- a/core/src/test/java/feign/TargetTest.java +++ b/core/src/test/java/feign/TargetTest.java @@ -66,8 +66,8 @@ public Request apply(RequestTemplate input) { urlEncoded.httpMethod(), urlEncoded.url().replace("%2F", "/"), urlEncoded.headers(), - urlEncoded.body().orElse(null), - null); + urlEncoded.body(), + urlEncoded.charset()); } }; diff --git a/core/src/test/java/feign/assertj/RequestTemplateAssert.java b/core/src/test/java/feign/assertj/RequestTemplateAssert.java index d3ccb7a7b9..0a571ebb36 100644 --- a/core/src/test/java/feign/assertj/RequestTemplateAssert.java +++ b/core/src/test/java/feign/assertj/RequestTemplateAssert.java @@ -15,11 +15,9 @@ */ package feign.assertj; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static feign.Util.UTF_8; -import feign.Request; import feign.RequestTemplate; -import java.nio.charset.StandardCharsets; import org.assertj.core.api.AbstractAssert; import org.assertj.core.data.MapEntry; import org.assertj.core.internal.ByteArrays; @@ -60,8 +58,7 @@ public RequestTemplateAssert hasBody(String utf8Expected) { if (actual.bodyTemplate() != null) { failWithMessage("\nExpecting bodyTemplate to be null, but was:<%s>", actual.bodyTemplate()); } - objects.assertEqual( - info, actual.requestBody().map(this::bodyAsUtf8String).orElse(null), utf8Expected); + objects.assertEqual(info, new String(actual.body(), UTF_8), utf8Expected); return this; } @@ -70,13 +67,13 @@ public RequestTemplateAssert hasBody(byte[] expected) { if (actual.bodyTemplate() != null) { failWithMessage("\nExpecting bodyTemplate to be null, but was:<%s>", actual.bodyTemplate()); } - arrays.assertContains(info, actual.requestBody().map(this::bodyAsBytes).orElse(null), expected); + arrays.assertContains(info, actual.body(), expected); return this; } public RequestTemplateAssert hasBodyTemplate(String expected) { isNotNull(); - if (actual.requestBody().isPresent()) { + if (actual.body() != null) { failWithMessage("\nExpecting body to be null, but was:<%s>", actual.bodyTemplate()); } objects.assertEqual(info, actual.bodyTemplate(), expected); @@ -102,24 +99,17 @@ public RequestTemplateAssert hasNoHeader(final String encoded) { public RequestTemplateAssert noRequestBody() { isNotNull(); - if (actual.requestBody().isPresent()) { + if (actual.body() != null) { if (actual.bodyTemplate() != null) { failWithMessage( "\nExpecting requestBody.bodyTemplate to be null, but was:<%s>", actual.bodyTemplate()); } - if (actual.requestBody().isPresent()) { + if (actual.body() != null) { failWithMessage( - "\nExpecting requestBody.data to be null, but was:<%s>", actual.requestBody().get()); + "\nExpecting requestBody.data to be null, but was:<%s>", + new String(actual.body(), actual.requestCharset())); } } return this; } - - private String bodyAsUtf8String(Request.Body body) { - return assertDoesNotThrow(() -> body.writeToString(StandardCharsets.UTF_8)); - } - - private byte[] bodyAsBytes(Request.Body body) { - return assertDoesNotThrow(body::writeToByteArray); - } } diff --git a/core/src/test/java/feign/codec/DefaultDecoderTest.java b/core/src/test/java/feign/codec/DefaultDecoderTest.java index f1856c2946..23ba1d4d06 100644 --- a/core/src/test/java/feign/codec/DefaultDecoderTest.java +++ b/core/src/test/java/feign/codec/DefaultDecoderTest.java @@ -22,6 +22,7 @@ import feign.Request; import feign.Request.HttpMethod; import feign.Response; +import feign.Util; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.Collection; @@ -73,7 +74,7 @@ private Response knownResponse() { .status(200) .reason("OK") .headers(headers) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .body(inputStream, content.length()) .build(); } @@ -83,7 +84,7 @@ private Response nullBodyResponse() { .status(200) .reason("OK") .headers(Collections.>emptyMap()) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .build(); } } diff --git a/core/src/test/java/feign/codec/DefaultEncoderTest.java b/core/src/test/java/feign/codec/DefaultEncoderTest.java index 94f5183229..9aecbb9059 100644 --- a/core/src/test/java/feign/codec/DefaultEncoderTest.java +++ b/core/src/test/java/feign/codec/DefaultEncoderTest.java @@ -15,15 +15,13 @@ */ package feign.codec; +import static feign.Util.UTF_8; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; -import feign.Request; import feign.RequestTemplate; -import java.nio.charset.StandardCharsets; import java.time.Clock; -import java.util.Optional; +import java.util.Arrays; import org.junit.jupiter.api.Test; class DefaultEncoderTest { @@ -35,11 +33,7 @@ void encodesStrings() throws Exception { String content = "This is my content"; RequestTemplate template = new RequestTemplate(); encoder.encode(content, String.class, template); - Optional optionalBody = template.requestBody(); - assertThat(optionalBody).isPresent(); - String body = - assertDoesNotThrow(() -> optionalBody.get().writeToString(StandardCharsets.UTF_8)); - assertThat(body).contains(content); + assertThat(new String(template.body(), UTF_8)).isEqualTo(content); } @Test @@ -47,10 +41,7 @@ void encodesByteArray() throws Exception { byte[] content = {12, 34, 56}; RequestTemplate template = new RequestTemplate(); encoder.encode(content, byte[].class, template); - Optional optionalBody = template.requestBody(); - assertThat(optionalBody).isPresent(); - byte[] body = assertDoesNotThrow(() -> optionalBody.get().writeToByteArray()); - assertThat(body).isEqualTo(content); + assertThat(Arrays.equals(content, template.body())).isTrue(); } @Test diff --git a/core/src/test/java/feign/codec/DefaultErrorDecoderHttpErrorTest.java b/core/src/test/java/feign/codec/DefaultErrorDecoderHttpErrorTest.java index 0d546b1cd7..c9dae156bd 100644 --- a/core/src/test/java/feign/codec/DefaultErrorDecoderHttpErrorTest.java +++ b/core/src/test/java/feign/codec/DefaultErrorDecoderHttpErrorTest.java @@ -141,7 +141,11 @@ void exceptionIsHttpSpecific(int httpStatus, Class expectedExceptionClass, Strin .reason("anything") .request( Request.create( - HttpMethod.GET, "http://example.com/api", Collections.emptyMap(), null, null)) + HttpMethod.GET, + "http://example.com/api", + Collections.emptyMap(), + null, + Util.UTF_8)) .headers(headers) .body("response body", Util.UTF_8) .build(); diff --git a/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java b/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java index 6088ea54b1..1967e77632 100644 --- a/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java +++ b/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java @@ -26,7 +26,6 @@ import feign.Util; import java.io.ByteArrayInputStream; import java.io.InputStream; -import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -48,7 +47,8 @@ void throwsFeignException() throws Throwable { Response.builder() .status(500) .reason("Internal server error") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(headers) .build(); @@ -63,7 +63,8 @@ void throwsFeignExceptionIncludingBody() throws Throwable { Response.builder() .status(500) .reason("Internal server error") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(headers) .body("hello world", UTF_8) .build(); @@ -85,7 +86,8 @@ void throwsFeignExceptionIncludingLongBody() throws Throwable { Response.builder() .status(500) .reason("Internal server error") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(headers) .body(actualBody, UTF_8) .build(); @@ -117,7 +119,8 @@ void feignExceptionIncludesStatus() { Response.builder() .status(400) .reason("Bad request") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(headers) .build(); @@ -135,7 +138,8 @@ void retryAfterHeaderThrowsRetryableException() throws Throwable { Response.builder() .status(503) .reason("Service Unavailable") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(headers) .build(); @@ -190,7 +194,8 @@ private Response bigBodyResponse() { Request.HttpMethod.GET, "/home", Collections.emptyMap(), - Request.Body.of("data".getBytes(StandardCharsets.UTF_8)), + "data".getBytes(Util.UTF_8), + Util.UTF_8, null)) .body(content, Util.UTF_8) .build(); diff --git a/core/src/test/java/feign/stream/StreamDecoderTest.java b/core/src/test/java/feign/stream/StreamDecoderTest.java index 86a6d8509e..d514dcc6b7 100644 --- a/core/src/test/java/feign/stream/StreamDecoderTest.java +++ b/core/src/test/java/feign/stream/StreamDecoderTest.java @@ -24,6 +24,7 @@ import feign.Request.HttpMethod; import feign.RequestLine; import feign.Response; +import feign.Util; import java.io.BufferedReader; import java.io.Closeable; import java.io.IOException; @@ -137,7 +138,8 @@ void shouldCloseIteratorWhenStreamClosed() throws IOException { .status(200) .reason("OK") .headers(Collections.emptyMap()) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .body("", UTF_8) .build(); diff --git a/dropwizard-metrics4/src/main/java/feign/metrics4/MeteredEncoder.java b/dropwizard-metrics4/src/main/java/feign/metrics4/MeteredEncoder.java index 4263d14d51..2a2c644c5f 100644 --- a/dropwizard-metrics4/src/main/java/feign/metrics4/MeteredEncoder.java +++ b/dropwizard-metrics4/src/main/java/feign/metrics4/MeteredEncoder.java @@ -50,15 +50,13 @@ public void encode(Object object, Type bodyType, RequestTemplate template) encoder.encode(object, bodyType, template); } - template - .requestBody() - .ifPresent( - body -> - metricRegistry - .histogram( - metricName.metricName( - template.methodMetadata(), template.feignTarget(), "request_size"), - metricSuppliers.histograms()) - .update(body.contentLength())); + if (template.body() != null) { + metricRegistry + .histogram( + metricName.metricName( + template.methodMetadata(), template.feignTarget(), "request_size"), + metricSuppliers.histograms()) + .update(template.body().length); + } } } diff --git a/dropwizard-metrics5/src/main/java/feign/metrics5/MeteredEncoder.java b/dropwizard-metrics5/src/main/java/feign/metrics5/MeteredEncoder.java index 09f3058be4..77cc7b78cb 100644 --- a/dropwizard-metrics5/src/main/java/feign/metrics5/MeteredEncoder.java +++ b/dropwizard-metrics5/src/main/java/feign/metrics5/MeteredEncoder.java @@ -62,15 +62,13 @@ public void encode(Object object, Type bodyType, RequestTemplate template) encoder.encode(object, bodyType, template); } - template - .requestBody() - .ifPresent( - body -> - metricRegistry - .histogram( - metricName.metricName( - template.methodMetadata(), template.feignTarget(), "request_size"), - metricSuppliers.histograms()) - .update(body.contentLength())); + if (template.body() != null) { + metricRegistry + .histogram( + metricName.metricName( + template.methodMetadata(), template.feignTarget(), "request_size"), + metricSuppliers.histograms()) + .update(template.body().length); + } } } diff --git a/fastjson2/src/main/java/feign/fastjson2/Fastjson2Encoder.java b/fastjson2/src/main/java/feign/fastjson2/Fastjson2Encoder.java index 7e79af331a..06a98e8dd2 100644 --- a/fastjson2/src/main/java/feign/fastjson2/Fastjson2Encoder.java +++ b/fastjson2/src/main/java/feign/fastjson2/Fastjson2Encoder.java @@ -17,8 +17,8 @@ import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONWriter; -import feign.Request; import feign.RequestTemplate; +import feign.Util; import feign.codec.EncodeException; import feign.codec.Encoder; import feign.codec.JsonEncoder; @@ -42,6 +42,6 @@ public Fastjson2Encoder(JSONWriter.Feature[] features) { @Override public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { - template.body(Request.Body.of(JSON.toJSONBytes(object, features))); + template.body(JSON.toJSONBytes(object, features), Util.UTF_8); } } diff --git a/fastjson2/src/test/java/feign/fastjson2/FastJsonCodecTest.java b/fastjson2/src/test/java/feign/fastjson2/FastJsonCodecTest.java index 06c00502d5..606ccf2511 100644 --- a/fastjson2/src/test/java/feign/fastjson2/FastJsonCodecTest.java +++ b/fastjson2/src/test/java/feign/fastjson2/FastJsonCodecTest.java @@ -22,6 +22,7 @@ import feign.Request; import feign.RequestTemplate; import feign.Response; +import feign.Util; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -106,7 +107,8 @@ void decodes() throws Exception { .status(200) .reason("OK") .request( - Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + Request.create( + Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(zonesJson, UTF_8) .build(); @@ -122,7 +124,8 @@ void nullBodyDecodesToNull() throws Exception { .status(204) .reason("OK") .request( - Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + Request.create( + Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .build(); assertThat(new Fastjson2Decoder().decode(response, String.class)).isNull(); @@ -135,7 +138,8 @@ void emptyBodyDecodesToNull() throws Exception { .status(204) .reason("OK") .request( - Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + Request.create( + Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(new byte[0]) .build(); @@ -154,7 +158,8 @@ void decoderCharset() throws IOException { .status(200) .reason("OK") .request( - Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + Request.create( + Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(headers) .body( new String( @@ -200,7 +205,8 @@ void notFoundDecodesToEmpty() throws Exception { .status(404) .reason("NOT FOUND") .request( - Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + Request.create( + Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .build(); assertThat((byte[]) new Fastjson2Decoder().decode(response, byte[].class)).isEmpty(); diff --git a/form/src/main/java/feign/form/MultipartFormContentProcessor.java b/form/src/main/java/feign/form/MultipartFormContentProcessor.java index d8c6e9228f..af31d36eb6 100644 --- a/form/src/main/java/feign/form/MultipartFormContentProcessor.java +++ b/form/src/main/java/feign/form/MultipartFormContentProcessor.java @@ -18,7 +18,6 @@ import static feign.form.ContentType.MULTIPART; import static lombok.AccessLevel.PRIVATE; -import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; @@ -104,7 +103,7 @@ public void process(RequestTemplate template, Charset charset, MapemptyList()); // reset header template.header(CONTENT_TYPE_HEADER, contentTypeValue); - template.body(Request.Body.of(bytes)); + template.body(bytes, charset); } @Override diff --git a/form/src/main/java/feign/form/multipart/DelegateWriter.java b/form/src/main/java/feign/form/multipart/DelegateWriter.java index 3295741e4e..7161c21a46 100644 --- a/form/src/main/java/feign/form/multipart/DelegateWriter.java +++ b/form/src/main/java/feign/form/multipart/DelegateWriter.java @@ -17,12 +17,9 @@ import static lombok.AccessLevel.PRIVATE; -import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; -import java.io.IOException; -import java.nio.charset.StandardCharsets; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; import lombok.val; @@ -49,15 +46,8 @@ public boolean isApplicable(Object value) { protected void write(Output output, String key, Object value) throws EncodeException { val fake = new RequestTemplate(); delegate.encode(value, value.getClass(), fake); - fake.requestBody().ifPresent(body -> write(output, key, body)); - } - - private void write(Output output, String key, Request.Body body) { - try { - val encoded = body.writeToString(StandardCharsets.UTF_8).replaceAll("\n", ""); - parameterWriter.write(output, key, encoded); - } catch (IOException e) { - throw new EncodeException("Failed to write request body for key: " + key, e); - } + val bytes = fake.body(); + val string = new String(bytes, output.getCharset()).replaceAll("\n", ""); + parameterWriter.write(output, key, string); } } diff --git a/googlehttpclient/src/main/java/feign/googlehttpclient/GoogleHttpClient.java b/googlehttpclient/src/main/java/feign/googlehttpclient/GoogleHttpClient.java index 65aa8f1180..a57101a89c 100644 --- a/googlehttpclient/src/main/java/feign/googlehttpclient/GoogleHttpClient.java +++ b/googlehttpclient/src/main/java/feign/googlehttpclient/GoogleHttpClient.java @@ -15,6 +15,7 @@ */ package feign.googlehttpclient; +import com.google.api.client.http.ByteArrayContent; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpContent; import com.google.api.client.http.HttpHeaders; @@ -27,7 +28,6 @@ import feign.Request; import feign.Response; import java.io.IOException; -import java.io.OutputStream; import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -66,7 +66,18 @@ public final Response execute(final Request inputRequest, final Request.Options private final HttpRequest convertRequest( final Request inputRequest, final Request.Options options) throws IOException { - final HttpContent content = toHttpContent(inputRequest); + // Setup the request body + HttpContent content = null; + if (inputRequest.length() > 0) { + final Collection contentTypeValues = inputRequest.headers().get("Content-Type"); + String contentType = null; + if (contentTypeValues != null && contentTypeValues.size() > 0) { + contentType = contentTypeValues.iterator().next(); + } else { + contentType = "application/octet-stream"; + } + content = new ByteArrayContent(contentType, inputRequest.body()); + } // Build the request final HttpRequest request = @@ -97,21 +108,6 @@ private final HttpRequest convertRequest( return request; } - private HttpContent toHttpContent(final Request inputRequest) { - if (!inputRequest.httpMethod().isWithBody() || !inputRequest.body().isPresent()) { - return null; - } - - final Request.Body requestBody = inputRequest.body().get(); - final Collection contentTypeValues = inputRequest.headers().get("Content-Type"); - final String contentType = - contentTypeValues != null && !contentTypeValues.isEmpty() - ? contentTypeValues.stream().findFirst().get() - : "application/octet-stream"; - - return new FeignBodyContent(requestBody, contentType); - } - private final Response convertResponse( final Request inputRequest, final HttpResponse inputResponse) throws IOException { final HttpHeaders headers = inputResponse.getHeaders(); @@ -135,34 +131,4 @@ private final Map> toMap(final HttpHeaders headers) { } return map; } - - private static final class FeignBodyContent implements HttpContent { - private final Request.Body body; - private final String contentType; - - private FeignBodyContent(Request.Body body, String contentType) { - this.body = body; - this.contentType = contentType; - } - - @Override - public long getLength() { - return body.contentLength(); - } - - @Override - public String getType() { - return contentType; - } - - @Override - public boolean retrySupported() { - return body.isRepeatable(); - } - - @Override - public void writeTo(OutputStream outputStream) throws IOException { - body.writeTo(outputStream); - } - } } diff --git a/graphql/src/main/java/feign/graphql/GraphqlDecoder.java b/graphql/src/main/java/feign/graphql/GraphqlDecoder.java index 78d8d25efe..4486a239f4 100644 --- a/graphql/src/main/java/feign/graphql/GraphqlDecoder.java +++ b/graphql/src/main/java/feign/graphql/GraphqlDecoder.java @@ -16,7 +16,6 @@ package feign.graphql; import feign.Experimental; -import feign.Request; import feign.Response; import feign.Util; import feign.codec.Decoder; @@ -112,14 +111,14 @@ private String resolveOperationField(Map root, Response response } } - if (response.request() != null && response.request().body().isPresent()) { + if (response.request() != null && response.request().body() != null) { try { var fakeResponse = Response.builder() .status(200) .headers(Collections.emptyMap()) .request(response.request()) - .body(bodyAsByteArray(response.request())) + .body(response.request().body()) .build(); var requestBody = (Map) jsonDecoder.decode(fakeResponse, Map.class); if (requestBody != null) { @@ -136,12 +135,6 @@ private String resolveOperationField(Map root, Response response return "unknown"; } - private byte[] bodyAsByteArray(Request request) throws IOException { - var body = request.body(); - - return body.isPresent() ? body.get().writeToByteArray() : null; - } - private boolean isOptionalType(Type type) { if (type instanceof ParameterizedType pt && pt.getRawType() instanceof Class cls) { return cls == Optional.class; diff --git a/graphql/src/main/java/feign/graphql/GraphqlRequestInterceptor.java b/graphql/src/main/java/feign/graphql/GraphqlRequestInterceptor.java index 5a318958ca..252834fc3d 100644 --- a/graphql/src/main/java/feign/graphql/GraphqlRequestInterceptor.java +++ b/graphql/src/main/java/feign/graphql/GraphqlRequestInterceptor.java @@ -34,7 +34,7 @@ public GraphqlRequestInterceptor(Encoder delegate, GraphqlContract contract) { @Override public void apply(RequestTemplate template) { - if (template.requestBody().isPresent()) { + if (template.body() != null) { return; } diff --git a/graphql/src/test/java/feign/graphql/GraphqlDecoderTest.java b/graphql/src/test/java/feign/graphql/GraphqlDecoderTest.java index 13d92f3aac..b796a68612 100644 --- a/graphql/src/test/java/feign/graphql/GraphqlDecoderTest.java +++ b/graphql/src/test/java/feign/graphql/GraphqlDecoderTest.java @@ -332,7 +332,11 @@ private Response buildResponse(String body) { private Request buildRequest() { return Request.create( - HttpMethod.POST, "http://localhost/graphql", Collections.emptyMap(), null, null); + HttpMethod.POST, + "http://localhost/graphql", + Collections.emptyMap(), + Request.Body.empty(), + null); } private static ParameterizedType optionalOf(Type inner) { diff --git a/graphql/src/test/java/feign/graphql/GraphqlEncoderTest.java b/graphql/src/test/java/feign/graphql/GraphqlEncoderTest.java index 21444de39a..07a22ed8a9 100644 --- a/graphql/src/test/java/feign/graphql/GraphqlEncoderTest.java +++ b/graphql/src/test/java/feign/graphql/GraphqlEncoderTest.java @@ -16,13 +16,10 @@ package feign.graphql; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import com.fasterxml.jackson.databind.ObjectMapper; -import feign.Request; import feign.RequestTemplate; import feign.jackson.JacksonEncoder; -import java.nio.charset.StandardCharsets; import java.util.Map; import org.junit.jupiter.api.Test; @@ -54,29 +51,13 @@ private RequestTemplate templateFor(Class apiClass) { return template; } - private byte[] bodyAsByteArray(Request.Body body) { - return assertDoesNotThrow(body::writeToByteArray); - } - - private String bodyAsUtf8String(Request.Body body) { - return assertDoesNotThrow(() -> body.writeToString(StandardCharsets.UTF_8)); - } - - private byte[] requestBodyBytes(RequestTemplate template) { - return template.requestBody().map(this::bodyAsByteArray).orElse(null); - } - - private String requestBodyString(RequestTemplate template) { - return template.requestBody().map(this::bodyAsUtf8String).orElse(null); - } - @Test void encodesBodyWithVariables() throws Exception { var template = templateFor(MutationApi.class); var body = Map.of("name", "John", "email", "john@example.com"); encoder.encode(body, Map.class, template); - var result = mapper.readTree(requestBodyBytes(template)); + var result = mapper.readTree(template.body()); assertThat(result.has("query")).isTrue(); assertThat(result.get("query").asText()).contains("createUser"); assertThat(result.has("variables")).isTrue(); @@ -88,7 +69,7 @@ void encodesBodyWithVariables() throws Exception { void delegatesToWrappedEncoderForNonGraphql() { var template = new RequestTemplate(); encoder.encode("plain body", String.class, template); - assertThat(template.requestBody()).isNotEmpty(); + assertThat(template.body()).isNotNull(); } @Test @@ -96,7 +77,7 @@ void interceptorSetsBodyForNoVariableQuery() throws Exception { var template = templateFor(NoVariableApi.class); interceptor.apply(template); - var result = mapper.readTree(requestBodyBytes(template)); + var result = mapper.readTree(template.body()); assertThat(result.get("query").asText()).contains("pending"); assertThat(result.has("variables")).isFalse(); } @@ -104,16 +85,16 @@ void interceptorSetsBodyForNoVariableQuery() throws Exception { @Test void interceptorSkipsWhenBodyAlreadySet() { var template = templateFor(MutationApi.class); - template.body(Request.Body.of("already set")); + template.body("already set"); interceptor.apply(template); - assertThat(requestBodyString(template)).isEqualTo("already set"); + assertThat(new String(template.body())).isEqualTo("already set"); } @Test void interceptorSkipsForNonGraphql() { var template = new RequestTemplate(); - template.body(Request.Body.of("some body")); + template.body("some body"); interceptor.apply(template); - assertThat(requestBodyString(template)).isEqualTo("some body"); + assertThat(new String(template.body())).isEqualTo("some body"); } } diff --git a/gson/src/main/java/feign/gson/GsonEncoder.java b/gson/src/main/java/feign/gson/GsonEncoder.java index c0c3d0eb2f..c4484bc6eb 100644 --- a/gson/src/main/java/feign/gson/GsonEncoder.java +++ b/gson/src/main/java/feign/gson/GsonEncoder.java @@ -17,7 +17,6 @@ import com.google.gson.Gson; import com.google.gson.TypeAdapter; -import feign.Request; import feign.RequestTemplate; import feign.codec.Encoder; import feign.codec.JsonEncoder; @@ -42,6 +41,6 @@ public GsonEncoder(Gson gson) { @Override public void encode(Object object, Type bodyType, RequestTemplate template) { - template.body(Request.Body.of(gson.toJson(object, bodyType))); + template.body(gson.toJson(object, bodyType)); } } diff --git a/gson/src/test/java/feign/gson/GsonCodecTest.java b/gson/src/test/java/feign/gson/GsonCodecTest.java index f1a99670bb..1ec47b8918 100644 --- a/gson/src/test/java/feign/gson/GsonCodecTest.java +++ b/gson/src/test/java/feign/gson/GsonCodecTest.java @@ -27,6 +27,7 @@ import feign.Request.HttpMethod; import feign.RequestTemplate; import feign.Response; +import feign.Util; import java.io.IOException; import java.util.Arrays; import java.util.Collections; @@ -65,7 +66,8 @@ void decodesMapObjectNumericalValuesAsInteger() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body("{\"foo\": 1}", UTF_8) .build(); @@ -129,7 +131,8 @@ void decodes() throws Exception { .status(200) .reason("OK") .headers(Collections.emptyMap()) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .body(zonesJson, UTF_8) .build(); assertThat(new GsonDecoder().decode(response, new TypeToken>() {}.getType())) @@ -143,7 +146,8 @@ void nullBodyDecodesToNull() throws Exception { .status(204) .reason("OK") .headers(Collections.emptyMap()) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .build(); assertThat(new GsonDecoder().decode(response, String.class)).isNull(); } @@ -155,7 +159,8 @@ void emptyBodyDecodesToNull() throws Exception { .status(204) .reason("OK") .headers(Collections.emptyMap()) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .body(new byte[0]) .build(); assertThat(new GsonDecoder().decode(response, String.class)).isNull(); @@ -211,7 +216,8 @@ void customDecoder() throws Exception { .status(200) .reason("OK") .headers(Collections.emptyMap()) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .body(zonesJson, UTF_8) .build(); assertThat(decoder.decode(response, new TypeToken>() {}.getType())).isEqualTo(zones); @@ -250,7 +256,8 @@ void notFoundDecodesToEmpty() throws Exception { .status(404) .reason("NOT FOUND") .headers(Collections.emptyMap()) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .build(); assertThat((byte[]) new GsonDecoder().decode(response, byte[].class)).isEmpty(); } diff --git a/hc5/src/main/java/feign/hc5/ApacheHttp5Client.java b/hc5/src/main/java/feign/hc5/ApacheHttp5Client.java index daaac3b77a..65edd9e71c 100644 --- a/hc5/src/main/java/feign/hc5/ApacheHttp5Client.java +++ b/hc5/src/main/java/feign/hc5/ApacheHttp5Client.java @@ -49,6 +49,7 @@ import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.http.io.entity.ByteArrayEntity; import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; import org.apache.hc.core5.net.URIBuilder; import org.apache.hc.core5.net.URLEncodedUtils; @@ -156,8 +157,22 @@ ClassicHttpRequest toClassicHttpRequest(Request request, Request.Options options } // request body - if (request.body().isPresent()) { - HttpEntity entity = new FeignBodyEntity(request.body().get(), getContentType(request)); + // final Body requestBody = request.requestBody(); + byte[] data = request.body(); + if (data != null) { + HttpEntity entity; + if (request.isBinary()) { + entity = new ByteArrayEntity(data, null); + } else { + final ContentType contentType = getContentType(request); + String content; + if (request.charset() != null) { + content = new String(data, request.charset()); + } else { + content = new String(data); + } + entity = new StringEntity(content, contentType); + } if (isGzip) { entity = new GzipCompressingEntity(entity); } @@ -176,6 +191,9 @@ private ContentType getContentType(Request request) { final Collection values = entry.getValue(); if (values != null && !values.isEmpty()) { contentType = ContentType.parse(values.iterator().next()); + if (contentType.getCharset() == null) { + contentType = contentType.withCharset(request.charset()); + } break; } } diff --git a/hc5/src/main/java/feign/hc5/AsyncApacheHttp5Client.java b/hc5/src/main/java/feign/hc5/AsyncApacheHttp5Client.java index 8a1adb5fd8..f51c1c2221 100644 --- a/hc5/src/main/java/feign/hc5/AsyncApacheHttp5Client.java +++ b/hc5/src/main/java/feign/hc5/AsyncApacheHttp5Client.java @@ -17,45 +17,24 @@ import static feign.Util.enumForName; -import feign.AsyncClient; -import feign.Request; +import feign.*; import feign.Request.Options; -import feign.Response; -import feign.Util; +import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; +import java.util.*; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; +import java.util.zip.GZIPOutputStream; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; -import org.apache.hc.client5.http.async.methods.SimpleResponseConsumer; import org.apache.hc.client5.http.config.Configurable; import org.apache.hc.client5.http.config.RequestConfig; -import org.apache.hc.client5.http.entity.GzipCompressingEntity; import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; import org.apache.hc.client5.http.impl.async.HttpAsyncClients; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.concurrent.FutureCallback; -import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpEntity; -import org.apache.hc.core5.http.NameValuePair; -import org.apache.hc.core5.http.io.entity.ByteArrayEntity; -import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; -import org.apache.hc.core5.http.nio.support.classic.ClassicToAsyncRequestProducer; import org.apache.hc.core5.io.CloseMode; -import org.apache.hc.core5.net.URIBuilder; -import org.apache.hc.core5.net.URLEncodedUtils; -import org.apache.hc.core5.util.Timeout; /** * This module directs Feign's http requests to Apache's @@ -71,34 +50,14 @@ public final class AsyncApacheHttp5Client implements AsyncClient { - Thread thread = new Thread(runnable, "feign-hc5-async-body-writer"); - thread.setDaemon(true); - return thread; - }); - private final CloseableHttpAsyncClient client; - private final Executor executor; - public AsyncApacheHttp5Client() { this(createStartedClient()); } public AsyncApacheHttp5Client(CloseableHttpAsyncClient client) { - this(client, DEFAULT_EXECUTOR); - } - - public AsyncApacheHttp5Client(CloseableHttpAsyncClient client, Executor executor) { - this.client = Objects.requireNonNull(client, "client must not be null"); - this.executor = Objects.requireNonNull(executor, "executor must not be null"); + this.client = client; } private static CloseableHttpAsyncClient createStartedClient() { @@ -110,15 +69,7 @@ private static CloseableHttpAsyncClient createStartedClient() { @Override public CompletableFuture execute( Request request, Options options, Optional requestContext) { - ClassicHttpRequest httpUriRequest; - try { - httpUriRequest = toClassicHttpRequest(request, options); - } catch (final URISyntaxException e) { - CompletableFuture failedFuture = new CompletableFuture<>(); - failedFuture.completeExceptionally( - new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", e)); - return failedFuture; - } + final SimpleHttpRequest httpUriRequest = toClassicHttpRequest(request, options); final CompletableFuture result = new CompletableFuture<>(); final FutureCallback callback = @@ -139,25 +90,12 @@ public void cancelled() { result.cancel(false); } }; - final ClassicToAsyncRequestProducer requestProducer = - new ClassicToAsyncRequestProducer( - httpUriRequest, Timeout.of(options.connectTimeout(), options.connectTimeoutUnit())); client.execute( - requestProducer, - SimpleResponseConsumer.create(), + httpUriRequest, configureTimeoutsAndRedirection(options, requestContext.orElseGet(HttpClientContext::new)), callback); - executor.execute( - () -> { - try { - requestProducer.blockWaiting().execute(); - } catch (IOException | InterruptedException e) { - result.completeExceptionally(e); - } - }); - return result; } @@ -176,20 +114,9 @@ protected HttpClientContext configureTimeoutsAndRedirection( return context; } - ClassicHttpRequest toClassicHttpRequest(Request request, Request.Options options) - throws URISyntaxException { - final ClassicRequestBuilder requestBuilder = - ClassicRequestBuilder.create(request.httpMethod().name()); - - final URI uri = new URIBuilder(request.url()).build(); - - requestBuilder.setUri(uri.getScheme() + "://" + uri.getAuthority() + uri.getRawPath()); - - // request query params - final List queryParams = URLEncodedUtils.parse(uri, requestBuilder.getCharset()); - for (final NameValuePair queryParam : queryParams) { - requestBuilder.addParameter(queryParam); - } + SimpleHttpRequest toClassicHttpRequest(Request request, Request.Options options) { + final SimpleHttpRequest httpRequest = + new SimpleHttpRequest(request.httpMethod().name(), request.url()); // request headers boolean hasAcceptHeader = false; @@ -215,27 +142,34 @@ ClassicHttpRequest toClassicHttpRequest(Request request, Request.Options options "Deflate Content-Encoding is not supported by feign-hc5"); } } + for (final String headerValue : headerEntry.getValue()) { - requestBuilder.addHeader(headerName, headerValue); + httpRequest.addHeader(headerName, headerValue); } } // some servers choke on the default accept string, so we'll set it to anything if (!hasAcceptHeader) { - requestBuilder.addHeader(ACCEPT_HEADER_NAME, "*/*"); + httpRequest.addHeader(ACCEPT_HEADER_NAME, "*/*"); } // request body - if (request.body().isPresent()) { - HttpEntity entity = new FeignBodyEntity(request.body().get(), getContentType(request)); - if (isGzip) { - entity = new GzipCompressingEntity(entity); + // final Body requestBody = request.requestBody(); + byte[] data = request.body(); + if (isGzip && data != null && data.length > 0) { + // compress if needed + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + GZIPOutputStream gzipOs = new GZIPOutputStream(baos, true)) { + gzipOs.write(data); + gzipOs.flush(); + data = baos.toByteArray(); + } catch (IOException suppressed) { // NOPMD } - requestBuilder.setEntity(entity); - } else { - requestBuilder.setEntity(new ByteArrayEntity(new byte[0], null)); + } + if (data != null) { + httpRequest.setBody(data, getContentType(request)); } - return requestBuilder.build(); + return httpRequest; } private ContentType getContentType(Request request) { @@ -245,6 +179,9 @@ private ContentType getContentType(Request request) { final Collection values = entry.getValue(); if (values != null && !values.isEmpty()) { contentType = ContentType.parse(values.iterator().next()); + if (contentType.getCharset() == null) { + contentType = contentType.withCharset(request.charset()); + } break; } } diff --git a/hc5/src/main/java/feign/hc5/FeignBodyEntity.java b/hc5/src/main/java/feign/hc5/FeignBodyEntity.java deleted file mode 100644 index 18504848df..0000000000 --- a/hc5/src/main/java/feign/hc5/FeignBodyEntity.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package feign.hc5; - -import feign.Request; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.io.entity.AbstractHttpEntity; - -/** - * A wrapper for {@link Request.Body} that implements Apache HttpClient's {@link - * AbstractHttpEntity}. - */ -final class FeignBodyEntity extends AbstractHttpEntity { - private final Request.Body body; - - /** - * Creates a new {@link FeignBodyEntity} with the given body and content type. - * - * @param body the body to wrap - * @param contentType the content type of the body - */ - FeignBodyEntity(Request.Body body, ContentType contentType) { - super(contentType, null, body.contentLength() < 0); - this.body = body; - } - - /** - * {@inheritDoc} - * - * @return {@inheritDoc} - */ - @Override - public long getContentLength() { - return body.contentLength(); - } - - /** - * {@inheritDoc} - * - * @return {@inheritDoc} - */ - @Override - public InputStream getContent() { - throw new UnsupportedOperationException("Streaming request body does not expose InputStream"); - } - - /** - * {@inheritDoc} - * - * @param outStream {@inheritDoc} - * @throws {@inheritDoc} - */ - @Override - public void writeTo(OutputStream outStream) throws IOException { - body.writeTo(outStream); - } - - /** - * {@inheritDoc} - * - * @return {@inheritDoc} - */ - @Override - public boolean isRepeatable() { - return body.isRepeatable(); - } - - /** - * {@inheritDoc} - * - * @return {@inheritDoc} - */ - @Override - public boolean isStreaming() { - return !isRepeatable(); - } - - /** Does nothing. The caller is responsible for closing the output stream. */ - @Override - public void close() {} -} diff --git a/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java b/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java index cb33e46dae..6d5902d9c0 100644 --- a/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java +++ b/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java @@ -17,6 +17,7 @@ import static feign.assertj.MockWebServerAssertions.assertThat; import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; import static org.assertj.core.data.MapEntry.entry; @@ -534,7 +535,7 @@ void throwsFeignExceptionIncludingBody() throws Throwable { } catch (final FeignException e) { assertThat(e.getMessage()) .isEqualTo("timeout reading POST http://localhost:" + server.getPort() + "/"); - assertThat(e.contentUTF8()).isEmpty(); + assertThat(e.contentUTF8()).isEqualTo("Request body"); return; } fail(""); @@ -571,7 +572,7 @@ void whenReturnTypeIsResponseNoErrorHandling() throws Throwable { .status(302) .reason("Found") .headers(headers) - .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, null)) + .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, Util.UTF_8)) .body(new byte[0]) .build(); @@ -772,7 +773,7 @@ private Response responseWithText(String text) { return Response.builder() .body(text, Util.UTF_8) .status(200) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(new HashMap<>()) .build(); } @@ -1083,9 +1084,9 @@ static final class TestInterfaceAsyncBuilder { .encoder( (object, _, template) -> { if (object instanceof Map) { - template.body(Request.Body.of(new Gson().toJson(object))); + template.body(new Gson().toJson(object)); } else { - template.body(Request.Body.of(object.toString())); + template.body(object.toString()); } }); diff --git a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java index 45e627bf27..010c229cb7 100644 --- a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java +++ b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java @@ -24,7 +24,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.OutputStream; import java.io.Reader; import java.net.URI; import java.net.URISyntaxException; @@ -41,15 +40,12 @@ import org.apache.http.StatusLine; import org.apache.http.client.HttpClient; import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.Configurable; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.client.methods.*; import org.apache.http.client.utils.URIBuilder; import org.apache.http.client.utils.URLEncodedUtils; -import org.apache.http.entity.AbstractHttpEntity; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; @@ -138,51 +134,24 @@ HttpUriRequest toHttpUriRequest(Request request, Request.Options options) } // request body - HttpEntity entity = - request - .body() - .map(body -> toHttpEntity(body, getContentType(request))) - .orElseGet(() -> new ByteArrayEntity(new byte[0])); + if (request.body() != null) { + HttpEntity entity = null; + if (request.charset() != null) { + ContentType contentType = getContentType(request); + String content = new String(request.body(), request.charset()); + entity = new StringEntity(content, contentType); + } else { + entity = new ByteArrayEntity(request.body()); + } - requestBuilder.setEntity(entity); + requestBuilder.setEntity(entity); + } else { + requestBuilder.setEntity(new ByteArrayEntity(new byte[0])); + } return requestBuilder.build(); } - private HttpEntity toHttpEntity(Request.Body body, ContentType contentType) { - AbstractHttpEntity httpEntity = - new AbstractHttpEntity() { - @Override - public long getContentLength() { - return body.contentLength(); - } - - @Override - public InputStream getContent() { - throw new UnsupportedOperationException( - "Streaming request body does not expose InputStream"); - } - - @Override - public void writeTo(OutputStream outStream) throws IOException { - body.writeTo(outStream); - } - - @Override - public boolean isRepeatable() { - return body.isRepeatable(); - } - - @Override - public boolean isStreaming() { - return !isRepeatable(); - } - }; - httpEntity.setContentType(contentType != null ? contentType.toString() : null); - httpEntity.setChunked(body.contentLength() < 0); - return httpEntity; - } - private ContentType getContentType(Request request) { ContentType contentType = null; for (Map.Entry> entry : request.headers().entrySet()) @@ -190,6 +159,9 @@ private ContentType getContentType(Request request) { Collection values = entry.getValue(); if (values != null && !values.isEmpty()) { contentType = ContentType.parse(values.iterator().next()); + if (contentType.getCharset() == null) { + contentType = contentType.withCharset(request.charset()); + } break; } } diff --git a/jackson-jaxb/src/main/java/feign/jackson/jaxb/JacksonJaxbJsonEncoder.java b/jackson-jaxb/src/main/java/feign/jackson/jaxb/JacksonJaxbJsonEncoder.java index 6e4caeaede..3786edb36e 100644 --- a/jackson-jaxb/src/main/java/feign/jackson/jaxb/JacksonJaxbJsonEncoder.java +++ b/jackson-jaxb/src/main/java/feign/jackson/jaxb/JacksonJaxbJsonEncoder.java @@ -20,13 +20,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; -import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.Type; +import java.nio.charset.Charset; public final class JacksonJaxbJsonEncoder implements Encoder { private final JacksonJaxbJsonProvider jacksonJaxbJsonProvider; @@ -46,7 +46,7 @@ public void encode(Object object, Type bodyType, RequestTemplate template) try { jacksonJaxbJsonProvider.writeTo( object, bodyType.getClass(), null, null, APPLICATION_JSON_TYPE, null, outputStream); - template.body(Request.Body.of(outputStream.toByteArray())); + template.body(outputStream.toByteArray(), Charset.defaultCharset()); } catch (IOException e) { throw new EncodeException(e.getMessage(), e); } diff --git a/jackson-jaxb/src/test/java/feign/jackson/jaxb/JacksonJaxbCodecTest.java b/jackson-jaxb/src/test/java/feign/jackson/jaxb/JacksonJaxbCodecTest.java index 29670826ea..8e0225d192 100644 --- a/jackson-jaxb/src/test/java/feign/jackson/jaxb/JacksonJaxbCodecTest.java +++ b/jackson-jaxb/src/test/java/feign/jackson/jaxb/JacksonJaxbCodecTest.java @@ -23,6 +23,7 @@ import feign.Request.HttpMethod; import feign.RequestTemplate; import feign.Response; +import feign.Util; import java.util.Collections; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; @@ -49,7 +50,8 @@ void decodeTest() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body("{\"value\":\"Test\"}", UTF_8) .build(); @@ -65,7 +67,8 @@ void notFoundDecodesToEmpty() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .build(); assertThat((byte[]) new JacksonJaxbJsonDecoder().decode(response, byte[].class)).isEmpty(); diff --git a/jackson-jr/src/main/java/feign/jackson/jr/JacksonJrEncoder.java b/jackson-jr/src/main/java/feign/jackson/jr/JacksonJrEncoder.java index 26fe78179c..44118eed76 100644 --- a/jackson-jr/src/main/java/feign/jackson/jr/JacksonJrEncoder.java +++ b/jackson-jr/src/main/java/feign/jackson/jr/JacksonJrEncoder.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.jr.ob.JSON; import com.fasterxml.jackson.jr.ob.JacksonJrExtension; -import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; @@ -54,9 +53,9 @@ public JacksonJrEncoder(Iterable iterable) { public void encode(Object object, Type bodyType, RequestTemplate template) { try { if (bodyType == byte[].class) { - template.body(Request.Body.of(mapper.asBytes(object))); + template.body(mapper.asBytes(object), null); } else { - template.body(Request.Body.of(mapper.asString(object))); + template.body(mapper.asString(object)); } } catch (IOException e) { throw new EncodeException(e.getMessage(), e); diff --git a/jackson-jr/src/test/java/feign/jackson/jr/JacksonCodecTest.java b/jackson-jr/src/test/java/feign/jackson/jr/JacksonCodecTest.java index c716bc8194..c7c70bd67b 100644 --- a/jackson-jr/src/test/java/feign/jackson/jr/JacksonCodecTest.java +++ b/jackson-jr/src/test/java/feign/jackson/jr/JacksonCodecTest.java @@ -25,6 +25,7 @@ import feign.Request.HttpMethod; import feign.RequestTemplate; import feign.Response; +import feign.Util; import java.io.IOException; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; @@ -95,7 +96,8 @@ void decodes() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(zonesJson, UTF_8) .build(); @@ -110,7 +112,8 @@ void nullBodyDecodesToEmpty() throws Exception { Response.builder() .status(204) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .build(); assertThat((byte[]) new JacksonJrDecoder().decode(response, byte[].class)).isEmpty(); @@ -122,7 +125,8 @@ void emptyBodyDecodesToEmpty() throws Exception { Response.builder() .status(204) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(new byte[0]) .build(); @@ -141,7 +145,8 @@ void customDecoder() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(DATES_JSON, UTF_8) .build(); @@ -162,7 +167,8 @@ void customDecoderExpressedAsMapper() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(DATES_JSON, UTF_8) .build(); @@ -195,7 +201,8 @@ void decoderCharset() throws IOException { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(headers) .body( new String( @@ -221,7 +228,8 @@ void decodesToMap() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(json, UTF_8) .build(); @@ -299,7 +307,8 @@ void notFoundDecodesToEmpty() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .build(); assertThat((byte[]) new JacksonJrDecoder().decode(response, byte[].class)).isEmpty(); @@ -339,7 +348,12 @@ static Stream decodeGenericsArguments() { Response.Builder responseBuilder = Response.builder() .request( - Request.create(HttpMethod.GET, "/v1/dummy", Collections.emptyMap(), null, null)); + Request.create( + HttpMethod.GET, + "/v1/dummy", + Collections.emptyMap(), + Request.Body.empty(), + null)); return Stream.of( Arguments.of( responseBuilder.body("{\"data\":2024}", StandardCharsets.UTF_8).build(), diff --git a/jackson/src/main/java/feign/jackson/JacksonEncoder.java b/jackson/src/main/java/feign/jackson/JacksonEncoder.java index 280cd3bb58..48b169a8d3 100644 --- a/jackson/src/main/java/feign/jackson/JacksonEncoder.java +++ b/jackson/src/main/java/feign/jackson/JacksonEncoder.java @@ -21,8 +21,8 @@ import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -import feign.Request; import feign.RequestTemplate; +import feign.Util; import feign.codec.EncodeException; import feign.codec.Encoder; import feign.codec.JsonEncoder; @@ -53,7 +53,7 @@ public JacksonEncoder(ObjectMapper mapper) { public void encode(Object object, Type bodyType, RequestTemplate template) { try { JavaType javaType = mapper.getTypeFactory().constructType(bodyType); - template.body(Request.Body.of(mapper.writerFor(javaType).writeValueAsBytes(object))); + template.body(mapper.writerFor(javaType).writeValueAsBytes(object), Util.UTF_8); } catch (JsonProcessingException e) { throw new EncodeException(e.getMessage(), e); } diff --git a/jackson/src/test/java/feign/jackson/JacksonCodecTest.java b/jackson/src/test/java/feign/jackson/JacksonCodecTest.java index 0097e066ee..7de8ec3ad4 100644 --- a/jackson/src/test/java/feign/jackson/JacksonCodecTest.java +++ b/jackson/src/test/java/feign/jackson/JacksonCodecTest.java @@ -32,6 +32,7 @@ import feign.Request.HttpMethod; import feign.RequestTemplate; import feign.Response; +import feign.Util; import java.io.Closeable; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -120,7 +121,8 @@ void decodes() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(zonesJson, UTF_8) .build(); @@ -134,7 +136,8 @@ void nullBodyDecodesToNull() throws Exception { Response.builder() .status(204) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .build(); assertThat(new JacksonDecoder().decode(response, String.class)).isNull(); @@ -146,7 +149,8 @@ void emptyBodyDecodesToNull() throws Exception { Response.builder() .status(204) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(new byte[0]) .build(); @@ -167,7 +171,8 @@ void customDecoder() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(zonesJson, UTF_8) .build(); @@ -215,7 +220,8 @@ void decoderCharset() throws IOException { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(headers) .body( new String( @@ -244,7 +250,8 @@ void decodesIterator() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(zonesJson, UTF_8) .build(); @@ -270,7 +277,8 @@ void nullBodyDecodesToEmptyIterator() throws Exception { Response.builder() .status(204) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .build(); assertThat((byte[]) JacksonIteratorDecoder.create().decode(response, byte[].class)).isEmpty(); @@ -282,7 +290,8 @@ void emptyBodyDecodesToEmptyIterator() throws Exception { Response.builder() .status(204) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(new byte[0]) .build(); @@ -355,7 +364,8 @@ void notFoundDecodesToEmpty() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .build(); assertThat((byte[]) new JacksonDecoder().decode(response, byte[].class)).isEmpty(); @@ -368,7 +378,8 @@ void notFoundDecodesToEmptyIterator() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .build(); assertThat((byte[]) JacksonIteratorDecoder.create().decode(response, byte[].class)).isEmpty(); diff --git a/jackson/src/test/java/feign/jackson/JacksonIteratorTest.java b/jackson/src/test/java/feign/jackson/JacksonIteratorTest.java index e935b6d0f8..48bc1be058 100644 --- a/jackson/src/test/java/feign/jackson/JacksonIteratorTest.java +++ b/jackson/src/test/java/feign/jackson/JacksonIteratorTest.java @@ -25,6 +25,7 @@ import feign.Request; import feign.Request.HttpMethod; import feign.Response; +import feign.Util; import feign.codec.DecodeException; import feign.jackson.JacksonIteratorDecoder.JacksonIterator; import java.io.ByteArrayInputStream; @@ -122,7 +123,8 @@ public void close() throws IOException { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(inputStream, jsonBytes.length) .build(); @@ -148,7 +150,8 @@ public void close() throws IOException { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(inputStream, jsonBytes.length) .build(); @@ -178,7 +181,8 @@ JacksonIterator iterator(Class type, String json) throws IOException { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(json, UTF_8) .build(); diff --git a/jackson3/src/main/java/feign/jackson3/Jackson3Encoder.java b/jackson3/src/main/java/feign/jackson3/Jackson3Encoder.java index 273e6d8f79..90342c0162 100644 --- a/jackson3/src/main/java/feign/jackson3/Jackson3Encoder.java +++ b/jackson3/src/main/java/feign/jackson3/Jackson3Encoder.java @@ -16,8 +16,8 @@ package feign.jackson3; import com.fasterxml.jackson.annotation.JsonInclude; -import feign.Request; import feign.RequestTemplate; +import feign.Util; import feign.codec.EncodeException; import feign.codec.Encoder; import feign.codec.JsonEncoder; @@ -55,7 +55,7 @@ public Jackson3Encoder(JsonMapper mapper) { public void encode(Object object, Type bodyType, RequestTemplate template) { try { JavaType javaType = mapper.getTypeFactory().constructType(bodyType); - template.body(Request.Body.of(mapper.writerFor(javaType).writeValueAsBytes(object))); + template.body(mapper.writerFor(javaType).writeValueAsBytes(object), Util.UTF_8); } catch (JacksonException e) { throw new EncodeException(e.getMessage(), e); } diff --git a/jackson3/src/test/java/feign/jackson3/Jackson3CodecTest.java b/jackson3/src/test/java/feign/jackson3/Jackson3CodecTest.java index 16236b2c6d..d91683033c 100644 --- a/jackson3/src/test/java/feign/jackson3/Jackson3CodecTest.java +++ b/jackson3/src/test/java/feign/jackson3/Jackson3CodecTest.java @@ -23,6 +23,7 @@ import feign.Request.HttpMethod; import feign.RequestTemplate; import feign.Response; +import feign.Util; import java.io.Closeable; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -121,7 +122,8 @@ void decodes() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(zonesJson, UTF_8) .build(); @@ -135,7 +137,8 @@ void nullBodyDecodesToNull() throws Exception { Response.builder() .status(204) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .build(); assertThat(new Jackson3Decoder().decode(response, String.class)).isNull(); @@ -147,7 +150,8 @@ void emptyBodyDecodesToNull() throws Exception { Response.builder() .status(204) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(new byte[0]) .build(); @@ -168,7 +172,8 @@ void customDecoder() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(zonesJson, UTF_8) .build(); @@ -216,7 +221,8 @@ void decoderCharset() throws IOException { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(headers) .body( new String( @@ -245,7 +251,8 @@ void decodesIterator() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(zonesJson, UTF_8) .build(); @@ -271,7 +278,8 @@ void nullBodyDecodesToEmptyIterator() throws Exception { Response.builder() .status(204) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .build(); assertThat((byte[]) Jackson3IteratorDecoder.create().decode(response, byte[].class)).isEmpty(); @@ -283,7 +291,8 @@ void emptyBodyDecodesToEmptyIterator() throws Exception { Response.builder() .status(204) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(new byte[0]) .build(); @@ -356,7 +365,8 @@ void notFoundDecodesToEmpty() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .build(); assertThat((byte[]) new Jackson3Decoder().decode(response, byte[].class)).isEmpty(); @@ -369,7 +379,8 @@ void notFoundDecodesToEmptyIterator() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .build(); assertThat((byte[]) Jackson3IteratorDecoder.create().decode(response, byte[].class)).isEmpty(); diff --git a/java11/src/main/java/feign/http2client/Http2Client.java b/java11/src/main/java/feign/http2client/Http2Client.java index f1f4bb49b8..40d4548a22 100644 --- a/java11/src/main/java/feign/http2client/Http2Client.java +++ b/java11/src/main/java/feign/http2client/Http2Client.java @@ -15,10 +15,7 @@ */ package feign.http2client; -import static feign.Util.CONTENT_ENCODING; -import static feign.Util.ENCODING_DEFLATE; -import static feign.Util.ENCODING_GZIP; -import static feign.Util.enumForName; +import static feign.Util.*; import feign.AsyncClient; import feign.Client; @@ -29,9 +26,6 @@ import feign.Util; import java.io.IOException; import java.io.InputStream; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; -import java.io.UncheckedIOException; import java.lang.ref.SoftReference; import java.net.URI; import java.net.URISyntaxException; @@ -58,9 +52,6 @@ import java.util.TreeSet; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.stream.Collectors; import java.util.zip.GZIPInputStream; @@ -68,23 +59,8 @@ public class Http2Client implements Client, AsyncClient { - /** - * Dedicated default executor for the blocking body-writing bridge. Avoids starving the JVM-wide - * {@link java.util.concurrent.ForkJoinPool#commonPool()} when a thread is pinned per in-flight - * request. - */ - private static final Executor DEFAULT_EXECUTOR = - Executors.newCachedThreadPool( - runnable -> { - Thread thread = new Thread(runnable, "feign-http2-body-writer"); - thread.setDaemon(true); - return thread; - }); - private final HttpClient client; - private final Executor executor; - private final Map> clients = new ConcurrentHashMap<>(); /** @@ -107,21 +83,12 @@ public Http2Client() { .build()); } - public Http2Client(HttpClient client) { - this(client, DEFAULT_EXECUTOR); - } - public Http2Client(Options options) { - this(options, DEFAULT_EXECUTOR); + this(newClientBuilder(options).version(Version.HTTP_2).build()); } - public Http2Client(Options options, Executor executor) { - this(newClientBuilder(options).version(Version.HTTP_2).build(), executor); - } - - public Http2Client(HttpClient client, Executor executor) { + public Http2Client(HttpClient client) { this.client = Util.checkNotNull(client, "HttpClient must not be null"); - this.executor = Util.checkNotNull(executor, "Executor must not be null"); } @Override @@ -248,8 +215,13 @@ private static java.net.http.HttpClient.Builder newClientBuilder(Options options private Builder newRequestBuilder(Request request, Options options) throws URISyntaxException { URI uri = new URI(request.url()); - final BodyPublisher body = - request.body().map(this::createBodyPublisher).orElseGet(BodyPublishers::noBody); + final BodyPublisher body; + final byte[] data = request.body(); + if (data == null) { + body = BodyPublishers.noBody(); + } else { + body = BodyPublishers.ofByteArray(data); + } final Builder requestBuilder = HttpRequest.newBuilder() @@ -265,31 +237,6 @@ private Builder newRequestBuilder(Request request, Options options) throws URISy return requestBuilder.method(request.httpMethod().toString(), body); } - private BodyPublisher createBodyPublisher(Request.Body body) { - BodyPublisher publisher = - BodyPublishers.ofInputStream( - () -> { - PropagatingPipedInputStream inputStream = new PropagatingPipedInputStream(); - try { - PipedOutputStream outputStream = new PipedOutputStream(inputStream); - executor.execute( - () -> { - try (outputStream) { - body.writeTo(outputStream); - } catch (IOException e) { - inputStream.setException(e); - } - }); - return inputStream; - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); - return body.contentLength() > 0 - ? BodyPublishers.fromPublisher(publisher, body.contentLength()) - : publisher; - } - /** * There is a bunch o headers that the http2 client do not allow to be set. * @@ -331,41 +278,4 @@ private String[] asString(Map> headers) { .flatMap(List::stream)) .toArray(String[]::new); } - - /** - * A {@link PipedInputStream} that allows a writer thread to record an {@link IOException} via - * {@link #setException(IOException)} so subsequent reader calls throw an {@link IOException} - * wrapping the original cause; used to propagate write failures from the body-writer thread to - * the HTTP client reader. - */ - private static class PropagatingPipedInputStream extends PipedInputStream { - private final AtomicReference exception = new AtomicReference<>(); - - @Override - public synchronized int read() throws IOException { - checkException(); - int result = super.read(); - checkException(); - return result; - } - - @Override - public synchronized int read(byte[] b, int off, int len) throws IOException { - checkException(); - int result = super.read(b, off, len); - checkException(); - return result; - } - - public void setException(IOException e) { - exception.set(e); - } - - private void checkException() throws IOException { - IOException e = exception.get(); - if (e != null) { - throw new IOException("Body write failed", e); - } - } - } } diff --git a/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java b/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java index d205f51470..0d3cd6570b 100644 --- a/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java +++ b/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java @@ -16,6 +16,7 @@ package feign.http2client.test; import static feign.assertj.MockWebServerAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; import static org.assertj.core.data.MapEntry.entry; @@ -528,7 +529,7 @@ void throwsFeignExceptionIncludingBody() throws Throwable { } catch (final FeignException e) { assertThat(e.getMessage()) .isEqualTo("timeout reading POST http://localhost:" + server.getPort() + "/"); - assertThat(e.contentUTF8()).isEmpty(); + assertThat(e.contentUTF8()).isEqualTo("Request body"); return; } fail(""); @@ -565,7 +566,7 @@ void whenReturnTypeIsResponseNoErrorHandling() throws Throwable { .status(302) .reason("Found") .headers(headers) - .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, null)) + .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, Util.UTF_8)) .body(new byte[0]) .build(); @@ -769,7 +770,7 @@ private Response responseWithText(String text) { return Response.builder() .body(text, Util.UTF_8) .status(200) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(new HashMap<>()) .build(); } @@ -1027,9 +1028,9 @@ static final class TestInterfaceAsyncBuilder { .encoder( (object, _, template) -> { if (object instanceof Map) { - template.body(Request.Body.of(new Gson().toJson(object))); + template.body(new Gson().toJson(object)); } else { - template.body(Request.Body.of(object.toString())); + template.body(object.toString()); } }); diff --git a/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java b/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java index a25845800b..4ea3b7e998 100644 --- a/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java +++ b/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java @@ -15,7 +15,6 @@ */ package feign.jaxb; -import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; @@ -61,7 +60,7 @@ public void encode(Object object, Type bodyType, RequestTemplate template) { Marshaller marshaller = jaxbContextFactory.createMarshaller((Class) bodyType); StringWriter stringWriter = new StringWriter(); marshaller.marshal(object, stringWriter); - template.body(Request.Body.of(stringWriter.toString())); + template.body(stringWriter.toString()); } catch (JAXBException e) { throw new EncodeException(e.toString(), e); } diff --git a/jaxb-jakarta/src/test/java/feign/jaxb/JAXBCodecTest.java b/jaxb-jakarta/src/test/java/feign/jaxb/JAXBCodecTest.java index a4f62453de..d79d3c4725 100644 --- a/jaxb-jakarta/src/test/java/feign/jaxb/JAXBCodecTest.java +++ b/jaxb-jakarta/src/test/java/feign/jaxb/JAXBCodecTest.java @@ -17,13 +17,14 @@ import static feign.Util.UTF_8; import static feign.assertj.FeignAssertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import feign.Request; import feign.Request.HttpMethod; import feign.RequestTemplate; import feign.Response; +import feign.Util; import feign.codec.DecodeException; import feign.codec.EncodeException; import feign.codec.Encoder; @@ -198,7 +199,8 @@ void decodesXml() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(mockXml, UTF_8) .build(); @@ -221,7 +223,8 @@ class ParameterizedHolder { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body("", UTF_8) .build(); @@ -269,9 +272,10 @@ void decodeAnnotatedParameterizedTypes() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) - .body(template.requestBody().map(this::bodyAsBytes).orElse(null)) + .body(template.body()) .build(); new JAXBDecoder(new JAXBContextFactory.Builder().build()).decode(response, Box.class); @@ -284,7 +288,8 @@ void notFoundDecodesToEmpty() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .build(); assertThat( @@ -307,7 +312,8 @@ void decodeThrowsExceptionWhenUnmarshallingFailsWithSetSchema() throws Exception Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(mockXml, UTF_8) .build(); @@ -335,7 +341,8 @@ void decodesIgnoringErrorsWithEventHandler() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(mockXml, UTF_8) .build(); @@ -386,10 +393,6 @@ void encodesIgnoringErrorsWithEventHandler() throws Exception { """); } - private byte[] bodyAsBytes(Request.Body body) { - return assertDoesNotThrow(body::writeToByteArray); - } - @XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) static class MockIntObject { diff --git a/jaxb-jakarta/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java b/jaxb-jakarta/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java index b1f98bd8a2..d355afb691 100644 --- a/jaxb-jakarta/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java +++ b/jaxb-jakarta/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java @@ -16,12 +16,10 @@ package feign.jaxb.examples; import static feign.Util.UTF_8; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import feign.Request; import feign.RequestTemplate; import java.net.URI; -import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.time.Clock; import javax.crypto.Mac; @@ -74,7 +72,8 @@ private static String canonicalString(RequestTemplate input, String host) { canonicalRequest.append("host").append('\n'); // HexEncode(Hash(Payload)) - String bodyText = input.requestBody().map(AWSSignatureVersion4::bodyAsUtf8String).orElse(null); + byte[] data = input.body(); + String bodyText = (data != null) ? new String(data, input.requestCharset()) : null; if (bodyText != null) { canonicalRequest.append(hex(sha256(bodyText))); } else { @@ -83,10 +82,6 @@ private static String canonicalString(RequestTemplate input, String host) { return canonicalRequest.toString(); } - private static String bodyAsUtf8String(Request.Body body) { - return assertDoesNotThrow(() -> body.writeToString(StandardCharsets.UTF_8)); - } - private static String toSign(String timestamp, String credentialScope, String canonicalRequest) { StringBuilder toSign = new StringBuilder(); // Algorithm + '\n' + @@ -121,7 +116,7 @@ public Request apply(RequestTemplate input) { if (!input.headers().isEmpty()) { throw new UnsupportedOperationException("headers not supported"); } - if (input.requestBody().isPresent()) { + if (input.body() != null) { throw new UnsupportedOperationException("body not supported"); } diff --git a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java index 4c05d82e71..aae439cae6 100644 --- a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java +++ b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java @@ -15,7 +15,6 @@ */ package feign.jaxb; -import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; @@ -61,7 +60,7 @@ public void encode(Object object, Type bodyType, RequestTemplate template) { Marshaller marshaller = jaxbContextFactory.createMarshaller((Class) bodyType); StringWriter stringWriter = new StringWriter(); marshaller.marshal(object, stringWriter); - template.body(Request.Body.of(stringWriter.toString())); + template.body(stringWriter.toString()); } catch (JAXBException e) { throw new EncodeException(e.toString(), e); } diff --git a/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java b/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java index 139615175f..464d857b8c 100644 --- a/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java +++ b/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java @@ -17,13 +17,14 @@ import static feign.Util.UTF_8; import static feign.assertj.FeignAssertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import feign.Request; import feign.Request.HttpMethod; import feign.RequestTemplate; import feign.Response; +import feign.Util; import feign.codec.DecodeException; import feign.codec.EncodeException; import feign.codec.Encoder; @@ -198,7 +199,8 @@ void decodesXml() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(mockXml, UTF_8) .build(); @@ -221,7 +223,8 @@ class ParameterizedHolder { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body("", UTF_8) .build(); @@ -269,9 +272,10 @@ void decodeAnnotatedParameterizedTypes() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) - .body(template.requestBody().map(this::bodyAsBytes).orElse(null)) + .body(template.body()) .build(); new JAXBDecoder(new JAXBContextFactory.Builder().build()).decode(response, Box.class); @@ -284,7 +288,8 @@ void notFoundDecodesToEmpty() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .build(); assertThat( @@ -307,7 +312,8 @@ void decodeThrowsExceptionWhenUnmarshallingFailsWithSetSchema() throws Exception Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(mockXml, UTF_8) .build(); @@ -335,7 +341,8 @@ void decodesIgnoringErrorsWithEventHandler() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(mockXml, UTF_8) .build(); @@ -387,10 +394,6 @@ void encodesIgnoringErrorsWithEventHandler() throws Exception { """); } - private byte[] bodyAsBytes(Request.Body body) { - return assertDoesNotThrow(body::writeToByteArray); - } - @XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) static class MockIntObject { diff --git a/jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java b/jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java index b1f98bd8a2..d355afb691 100644 --- a/jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java +++ b/jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java @@ -16,12 +16,10 @@ package feign.jaxb.examples; import static feign.Util.UTF_8; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import feign.Request; import feign.RequestTemplate; import java.net.URI; -import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.time.Clock; import javax.crypto.Mac; @@ -74,7 +72,8 @@ private static String canonicalString(RequestTemplate input, String host) { canonicalRequest.append("host").append('\n'); // HexEncode(Hash(Payload)) - String bodyText = input.requestBody().map(AWSSignatureVersion4::bodyAsUtf8String).orElse(null); + byte[] data = input.body(); + String bodyText = (data != null) ? new String(data, input.requestCharset()) : null; if (bodyText != null) { canonicalRequest.append(hex(sha256(bodyText))); } else { @@ -83,10 +82,6 @@ private static String canonicalString(RequestTemplate input, String host) { return canonicalRequest.toString(); } - private static String bodyAsUtf8String(Request.Body body) { - return assertDoesNotThrow(() -> body.writeToString(StandardCharsets.UTF_8)); - } - private static String toSign(String timestamp, String credentialScope, String canonicalRequest) { StringBuilder toSign = new StringBuilder(); // Algorithm + '\n' + @@ -121,7 +116,7 @@ public Request apply(RequestTemplate input) { if (!input.headers().isEmpty()) { throw new UnsupportedOperationException("headers not supported"); } - if (input.requestBody().isPresent()) { + if (input.body() != null) { throw new UnsupportedOperationException("body not supported"); } diff --git a/jaxrs2/src/main/java/feign/jaxrs2/JAXRSClient.java b/jaxrs2/src/main/java/feign/jaxrs2/JAXRSClient.java index 555012f4b8..5ecdb94511 100644 --- a/jaxrs2/src/main/java/feign/jaxrs2/JAXRSClient.java +++ b/jaxrs2/src/main/java/feign/jaxrs2/JAXRSClient.java @@ -19,6 +19,7 @@ import feign.Request.Options; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.Charset; import java.util.Collection; import java.util.Map; import java.util.Map.Entry; @@ -31,7 +32,7 @@ import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; -import javax.ws.rs.core.StreamingOutput; +import javax.ws.rs.core.Variant; /** * This module directs Feign's http requests to javax.ws.rs.client.Client . Ex: @@ -76,11 +77,15 @@ public feign.Response execute(feign.Request request, Options options) throws IOE .build(); } - private Entity createRequestEntity(feign.Request request) { - return request - .body() - .map(body -> Entity.entity((StreamingOutput) body::writeTo, mediaType(request.headers()))) - .orElse(null); + private Entity createRequestEntity(feign.Request request) { + if (request.body() == null) { + return null; + } + + return Entity.entity( + request.body(), + new Variant( + mediaType(request.headers()), locale(request.headers()), encoding(request.charset()))); } private Integer integerHeader(Response response, String header) { @@ -97,16 +102,22 @@ private Integer integerHeader(Response response, String header) { } } + private String encoding(Charset charset) { + if (charset == null) return null; + + return charset.name(); + } + private String locale(Map> headers) { if (!headers.containsKey(HttpHeaders.CONTENT_LANGUAGE)) return null; return headers.get(HttpHeaders.CONTENT_LANGUAGE).iterator().next(); } - private String mediaType(Map> headers) { - if (!headers.containsKey(HttpHeaders.CONTENT_TYPE)) return MediaType.APPLICATION_OCTET_STREAM; + private MediaType mediaType(Map> headers) { + if (!headers.containsKey(HttpHeaders.CONTENT_TYPE)) return null; - return headers.get(HttpHeaders.CONTENT_TYPE).iterator().next(); + return MediaType.valueOf(headers.get(HttpHeaders.CONTENT_TYPE).iterator().next()); } private MultivaluedMap toMultivaluedMap(Map> headers) { diff --git a/json/src/main/java/feign/json/JsonEncoder.java b/json/src/main/java/feign/json/JsonEncoder.java index 1ef3cd6fb1..655bb7594a 100644 --- a/json/src/main/java/feign/json/JsonEncoder.java +++ b/json/src/main/java/feign/json/JsonEncoder.java @@ -17,7 +17,6 @@ import static java.lang.String.format; -import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; @@ -59,7 +58,7 @@ public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { if (object == null) return; if (object instanceof JSONArray || object instanceof JSONObject) { - template.body(Request.Body.of(object.toString())); + template.body(object.toString()); } else { throw new EncodeException(format("%s is not a type supported by this encoder.", bodyType)); } diff --git a/json/src/test/java/feign/json/JsonCodecTest.java b/json/src/test/java/feign/json/JsonCodecTest.java index 502b32eb9b..becd040ee5 100644 --- a/json/src/test/java/feign/json/JsonCodecTest.java +++ b/json/src/test/java/feign/json/JsonCodecTest.java @@ -17,7 +17,6 @@ import static feign.Util.toByteArray; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import feign.Feign; import feign.Param; @@ -28,7 +27,6 @@ import feign.mock.MockTarget; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.StandardCharsets; import org.json.JSONArray; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; @@ -82,9 +80,8 @@ void encodes() { "{\"login\":\"radio-rogal\",\"contributions\":0}"); JSONObject response = github.create("openfeign", "feign", contributor); Request request = mockClient.verifyOne(HttpMethod.POST, "/repos/openfeign/feign/contributors"); - assertThat(request.body()).isPresent(); - String json = - assertDoesNotThrow(() -> request.body().get().writeToString(StandardCharsets.UTF_8)); + assertThat(request.body()).isNotNull(); + String json = new String(request.body()); assertThat(json).contains("\"login\":\"radio-rogal\""); assertThat(json).contains("\"contributions\":0"); assertThat(response.getString("login")).isEqualTo("radio-rogal"); diff --git a/json/src/test/java/feign/json/JsonDecoderTest.java b/json/src/test/java/feign/json/JsonDecoderTest.java index 4147e3c469..1635aa7f67 100644 --- a/json/src/test/java/feign/json/JsonDecoderTest.java +++ b/json/src/test/java/feign/json/JsonDecoderTest.java @@ -26,6 +26,7 @@ import feign.Response; import feign.codec.DecodeException; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.time.Clock; import java.util.Collections; import org.json.JSONArray; @@ -53,7 +54,8 @@ static void setUpClass() { Request.HttpMethod.GET, "/qwerty", Collections.emptyMap(), - Request.Body.of("xyz"), + "xyz".getBytes(StandardCharsets.UTF_8), + StandardCharsets.UTF_8, null); } diff --git a/json/src/test/java/feign/json/JsonEncoderTest.java b/json/src/test/java/feign/json/JsonEncoderTest.java index d7cb951026..a8d2d1f360 100644 --- a/json/src/test/java/feign/json/JsonEncoderTest.java +++ b/json/src/test/java/feign/json/JsonEncoderTest.java @@ -15,14 +15,12 @@ */ package feign.json; +import static feign.Util.UTF_8; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; -import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; -import java.nio.charset.StandardCharsets; import java.time.Clock; import org.json.JSONArray; import org.json.JSONObject; @@ -51,24 +49,20 @@ void setUp() { void encodesArray() { new JsonEncoder().encode(jsonArray, JSONArray.class, requestTemplate); JSONAssert.assertEquals( - "[{\"a\":\"b\",\"c\":1},123]", - requestTemplate.requestBody().map(this::bodyAsUtf8String).orElse(null), - false); + "[{\"a\":\"b\",\"c\":1},123]", new String(requestTemplate.body(), UTF_8), false); } @Test void encodesObject() { new JsonEncoder().encode(jsonObject, JSONObject.class, requestTemplate); JSONAssert.assertEquals( - "{\"a\":\"b\",\"c\":1}", - requestTemplate.requestBody().map(this::bodyAsUtf8String).orElse(null), - false); + "{\"a\":\"b\",\"c\":1}", new String(requestTemplate.body(), UTF_8), false); } @Test void encodesNull() { new JsonEncoder().encode(null, JSONObject.class, new RequestTemplate()); - assertThat(requestTemplate.requestBody()).isEmpty(); + assertThat(requestTemplate.body()).isNull(); } @Test @@ -80,8 +74,4 @@ void unknownTypeThrowsEncodeException() { assertThat(exception.getMessage()) .isEqualTo("class java.time.Clock is not a type supported by this encoder."); } - - private String bodyAsUtf8String(Request.Body body) { - return assertDoesNotThrow(() -> body.writeToString(StandardCharsets.UTF_8)); - } } diff --git a/kotlin/src/test/kotlin/feign/kotlin/CoroutineFeignTest.kt b/kotlin/src/test/kotlin/feign/kotlin/CoroutineFeignTest.kt index 3f02011df5..aa1d25266b 100644 --- a/kotlin/src/test/kotlin/feign/kotlin/CoroutineFeignTest.kt +++ b/kotlin/src/test/kotlin/feign/kotlin/CoroutineFeignTest.kt @@ -19,7 +19,6 @@ import com.google.gson.Gson import com.google.gson.JsonIOException import feign.Param import feign.QueryMapEncoder -import feign.Request import feign.RequestInterceptor import feign.RequestLine import feign.Response @@ -178,9 +177,9 @@ class CoroutineFeignTest { private val delegate = CoroutineFeign.builder() .decoder(DefaultDecoder()).encoder { `object`, bodyType, template -> if (`object` is Map<*, *>) { - template.body(Request.Body.of(Gson().toJson(`object`))) + template.body(Gson().toJson(`object`)) } else { - template.body(Request.Body.of(`object`.toString())) + template.body(`object`.toString()) } } diff --git a/micrometer/src/main/java/feign/micrometer/MeteredEncoder.java b/micrometer/src/main/java/feign/micrometer/MeteredEncoder.java index 784aa86c3b..2fb73d12f5 100644 --- a/micrometer/src/main/java/feign/micrometer/MeteredEncoder.java +++ b/micrometer/src/main/java/feign/micrometer/MeteredEncoder.java @@ -15,19 +15,13 @@ */ package feign.micrometer; -import static feign.Util.CONTENT_LENGTH; import static feign.micrometer.MetricTagResolver.EMPTY_TAGS_ARRAY; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; -import io.micrometer.core.instrument.DistributionSummary; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Tag; -import io.micrometer.core.instrument.Tags; -import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.*; import java.lang.reflect.Type; -import java.util.Collections; /** Wrap feign {@link Encoder} with metrics. */ public class MeteredEncoder implements Encoder { @@ -58,11 +52,9 @@ public void encode(Object object, Type bodyType, RequestTemplate template) createTimer(object, bodyType, template) .record(() -> encoder.encode(object, bodyType, template)); - template.headers().getOrDefault(CONTENT_LENGTH, Collections.emptySet()).stream() - .findFirst() - .ifPresent( - contentLength -> - createSummary(object, bodyType, template).record(Long.parseLong(contentLength))); + if (template.body() != null) { + createSummary(object, bodyType, template).record(template.body().length); + } } protected Timer createTimer(Object object, Type bodyType, RequestTemplate template) { diff --git a/mock/src/main/java/feign/mock/RequestKey.java b/mock/src/main/java/feign/mock/RequestKey.java index 1d572875e3..50ed918e48 100644 --- a/mock/src/main/java/feign/mock/RequestKey.java +++ b/mock/src/main/java/feign/mock/RequestKey.java @@ -18,9 +18,10 @@ import static feign.Util.UTF_8; import feign.Request; -import java.io.IOException; +import feign.Util; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; +import java.nio.charset.Charset; import java.util.Arrays; import java.util.Collection; import java.util.Map; @@ -35,6 +36,8 @@ public static class Builder { private RequestHeaders headers; + private Charset charset; + private byte[] body; private Builder(HttpMethod method, String url) { @@ -53,6 +56,11 @@ public Builder headers(RequestHeaders headers) { return this; } + public Builder charset(Charset charset) { + this.charset = charset; + return this; + } + public Builder body(String body) { return body(body.getBytes(UTF_8)); } @@ -77,7 +85,7 @@ public static RequestKey create(Request request) { private static String buildUrl(Request request) { try { - return URLDecoder.decode(request.url(), UTF_8.name()); + return URLDecoder.decode(request.url(), Util.UTF_8.name()); } catch (final UnsupportedEncodingException e) { throw new RuntimeException(e); } @@ -89,12 +97,15 @@ private static String buildUrl(Request request) { private final RequestHeaders headers; + private final Charset charset; + private final byte[] body; private RequestKey(Builder builder) { this.method = builder.method; this.url = builder.url; this.headers = builder.headers; + this.charset = builder.charset; this.body = builder.body; } @@ -102,19 +113,8 @@ private RequestKey(Request request) { this.method = HttpMethod.valueOf(request.httpMethod().name()); this.url = buildUrl(request); this.headers = RequestHeaders.of(request.headers()); - this.body = - request - .body() - .filter(Request.Body::isRepeatable) - .map( - body -> { - try { - return body.writeToByteArray(); - } catch (IOException ignored) { - return null; - } - }) - .orElse(null); + this.charset = request.charset(); + this.body = request.body(); } public HttpMethod getMethod() { @@ -129,6 +129,10 @@ public RequestHeaders getHeaders() { return headers; } + public Charset getCharset() { + return charset; + } + public byte[] getBody() { return body; } @@ -167,8 +171,10 @@ public boolean equalsExtended(Object obj) { RequestKey other = (RequestKey) obj; boolean headersEqual = other.headers == null || headers == null || headers.equals(other.headers); - boolean bodyEqual = other.body == null || body == null || Arrays.equals(body, other.body); - return headersEqual && bodyEqual; + boolean charsetEqual = + other.charset == null || charset == null || charset.equals(other.charset); + boolean bodyEqual = other.body == null || body == null || Arrays.equals(other.body, body); + return headersEqual && charsetEqual && bodyEqual; } return false; } @@ -176,7 +182,10 @@ public boolean equalsExtended(Object obj) { @Override public String toString() { return String.format( - "Request [%s %s: %s headers]", - method, url, headers == null ? "without" : "with " + headers); + "Request [%s %s: %s headers and %s]", + method, + url, + headers == null ? "without" : "with " + headers, + charset == null ? "no charset" : "charset " + charset); } } diff --git a/mock/src/test/java/feign/mock/MockClientTest.java b/mock/src/test/java/feign/mock/MockClientTest.java index 3f14d92d90..77916f4194 100644 --- a/mock/src/test/java/feign/mock/MockClientTest.java +++ b/mock/src/test/java/feign/mock/MockClientTest.java @@ -15,10 +15,10 @@ */ package feign.mock; +import static feign.Util.UTF_8; import static feign.Util.toByteArray; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import feign.Body; import feign.Feign; @@ -36,9 +36,7 @@ import java.io.InputStream; import java.lang.reflect.Type; import java.net.HttpURLConnection; -import java.nio.charset.StandardCharsets; import java.util.List; -import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -99,6 +97,16 @@ public Object decode(Response response, Type type) void setup() throws IOException { try (InputStream input = getClass().getResourceAsStream("/fixtures/contributors.json")) { byte[] data = toByteArray(input); + RequestKey postContributorKey = + RequestKey.builder(HttpMethod.POST, "/repos/netflix/feign/contributors") + .charset(UTF_8) + .headers( + RequestHeaders.builder() + .add("Content-Length", "55") + .add("Content-Type", "application/json") + .build()) + .body("{\"login\":\"velo_at_github\",\"type\":\"preposterous hacker\"}") + .build(); mockClient = new MockClient(); github = Feign.builder() @@ -111,10 +119,7 @@ void setup() throws IOException { HttpMethod.GET, "/repos/netflix/feign/contributors?client_id=7 7", new ByteArrayInputStream(data)) - .ok( - HttpMethod.POST, - "/repos/netflix/feign/contributors", - "{\"login\":\"velo\",\"contributions\":0}") + .ok(postContributorKey, "{\"login\":\"velo\",\"contributions\":0}") .noContent(HttpMethod.PATCH, "/repos/velo/feign-mock/contributors") .add( HttpMethod.GET, @@ -175,6 +180,16 @@ void paramsEncoding() { @Test void verifyInvocation() { + RequestKey testRequestKey = + RequestKey.builder(HttpMethod.POST, "/repos/netflix/feign/contributors") + .headers( + RequestHeaders.builder() + .add("Content-Length", "55") + .add("Content-Type", "application/json") + .build()) + .body("{\"login\":\"velo_at_github\",\"type\":\"preposterous hacker\"}") + .build(); + Contributor contribution = github.create("netflix", "feign", "velo_at_github", "preposterous hacker"); // making sure it received a proper response @@ -185,12 +200,14 @@ void verifyInvocation() { List results = mockClient.verifyTimes(HttpMethod.POST, "/repos/netflix/feign/contributors", 1); assertThat(results).hasSize(1); + results = mockClient.verifyTimes(testRequestKey, 1); + assertThat(results).hasSize(1); - Optional body = - mockClient.verifyOne(HttpMethod.POST, "/repos/netflix/feign/contributors").body(); - assertThat(body).isPresent(); + assertThat(mockClient.verifyOne(testRequestKey).body()).isNotNull(); + byte[] body = mockClient.verifyOne(HttpMethod.POST, "/repos/netflix/feign/contributors").body(); + assertThat(body).isNotNull(); - String message = assertDoesNotThrow(() -> body.get().writeToString(StandardCharsets.UTF_8)); + String message = new String(body); assertThat(message).contains("velo_at_github"); assertThat(message).contains("preposterous hacker"); @@ -205,6 +222,7 @@ void verifyNone() { testRequestKey = RequestKey.builder(HttpMethod.POST, "/repos/netflix/feign/contributors") + .charset(UTF_8) .headers( RequestHeaders.builder() .add("Content-Length", "55") @@ -225,6 +243,7 @@ void verifyNone() { testRequestKey = RequestKey.builder(HttpMethod.POST, "/repos/netflix/feign/contributors") + .charset(UTF_8) .headers( RequestHeaders.builder() .add("Content-Length", "55") diff --git a/mock/src/test/java/feign/mock/RequestKeyTest.java b/mock/src/test/java/feign/mock/RequestKeyTest.java index 9b03f3c0eb..23d6d75fb2 100644 --- a/mock/src/test/java/feign/mock/RequestKeyTest.java +++ b/mock/src/test/java/feign/mock/RequestKeyTest.java @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat; import feign.Request; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; @@ -32,7 +33,12 @@ class RequestKeyTest { @BeforeEach void setUp() { RequestHeaders headers = RequestHeaders.builder().add("my-header", "val").build(); - requestKey = RequestKey.builder(HttpMethod.GET, "a").headers(headers).body("content").build(); + requestKey = + RequestKey.builder(HttpMethod.GET, "a") + .headers(headers) + .charset(StandardCharsets.UTF_16) + .body("content") + .build(); } @Test @@ -41,6 +47,7 @@ void builder() throws Exception { assertThat(requestKey.getUrl()).isEqualTo("a"); assertThat(requestKey.getHeaders().size()).isEqualTo(1); assertThat(requestKey.getHeaders().fetch("my-header")).isEqualTo(Arrays.asList("val")); + assertThat(requestKey.getCharset()).isEqualTo(StandardCharsets.UTF_16); } @SuppressWarnings("deprecation") @@ -49,13 +56,20 @@ void create() throws Exception { Map> map = new HashMap<>(); map.put("my-header", Arrays.asList("val")); Request request = - Request.create(Request.HttpMethod.GET, "a", map, Request.Body.of("content"), null); + Request.create( + Request.HttpMethod.GET, + "a", + map, + "content".getBytes(StandardCharsets.UTF_8), + StandardCharsets.UTF_16); requestKey = RequestKey.create(request); assertThat(requestKey.getMethod()).isEqualTo(HttpMethod.GET); assertThat(requestKey.getUrl()).isEqualTo("a"); assertThat(requestKey.getHeaders().size()).isEqualTo(1); assertThat(requestKey.getHeaders().fetch("my-header")).isEqualTo(Arrays.asList("val")); + assertThat(requestKey.getCharset()).isEqualTo(StandardCharsets.UTF_16); + assertThat(requestKey.getBody()).isEqualTo("content".getBytes(StandardCharsets.UTF_8)); } @Test @@ -103,7 +117,11 @@ void equalMinimum() { @Test void equalExtra() { RequestHeaders headers = RequestHeaders.builder().add("my-other-header", "other value").build(); - RequestKey requestKey2 = RequestKey.builder(HttpMethod.GET, "a").headers(headers).build(); + RequestKey requestKey2 = + RequestKey.builder(HttpMethod.GET, "a") + .headers(headers) + .charset(StandardCharsets.ISO_8859_1) + .build(); assertThat(requestKey.hashCode()).isEqualTo(requestKey2.hashCode()); assertThat(requestKey).isEqualTo(requestKey2); @@ -120,7 +138,11 @@ void equalsExtended() { @Test void equalsExtendedExtra() { RequestHeaders headers = RequestHeaders.builder().add("my-other-header", "other value").build(); - RequestKey requestKey2 = RequestKey.builder(HttpMethod.GET, "a").headers(headers).build(); + RequestKey requestKey2 = + RequestKey.builder(HttpMethod.GET, "a") + .headers(headers) + .charset(StandardCharsets.ISO_8859_1) + .build(); assertThat(requestKey.hashCode()).isEqualTo(requestKey2.hashCode()); assertThat(requestKey.equalsExtended(requestKey2)).isEqualTo(false); @@ -129,7 +151,7 @@ void equalsExtendedExtra() { @Test void testToString() throws Exception { assertThat(requestKey.toString()).startsWith("Request [GET a: "); - assertThat(requestKey.toString()).contains(" with my-header=[val] "); + assertThat(requestKey.toString()).contains(" with my-header=[val] ", " UTF-16]"); } @Test @@ -137,6 +159,7 @@ void toStringSimple() throws Exception { requestKey = RequestKey.builder(HttpMethod.GET, "a").build(); assertThat(requestKey.toString()).startsWith("Request [GET a: "); - assertThat(requestKey.toString()).contains(" without "); + assertThat(requestKey.toString()).contains(" without ", " no charset"); } } +// diff --git a/moshi/src/main/java/feign/moshi/MoshiEncoder.java b/moshi/src/main/java/feign/moshi/MoshiEncoder.java index fdd7507741..b65f705e27 100644 --- a/moshi/src/main/java/feign/moshi/MoshiEncoder.java +++ b/moshi/src/main/java/feign/moshi/MoshiEncoder.java @@ -17,7 +17,6 @@ import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; -import feign.Request; import feign.RequestTemplate; import feign.codec.Encoder; import feign.codec.JsonEncoder; @@ -42,6 +41,6 @@ public MoshiEncoder(Iterable> adapters) { @Override public void encode(Object object, Type bodyType, RequestTemplate template) { JsonAdapter jsonAdapter = moshi.adapter(bodyType).indent(" "); - template.body(Request.Body.of(jsonAdapter.toJson(object))); + template.body(jsonAdapter.toJson(object)); } } diff --git a/moshi/src/test/java/feign/moshi/MoshiDecoderTest.java b/moshi/src/test/java/feign/moshi/MoshiDecoderTest.java index 7aabefc708..885a57b6df 100644 --- a/moshi/src/test/java/feign/moshi/MoshiDecoderTest.java +++ b/moshi/src/test/java/feign/moshi/MoshiDecoderTest.java @@ -22,6 +22,7 @@ import com.squareup.moshi.Moshi; import feign.Request; import feign.Response; +import feign.Util; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedList; @@ -59,7 +60,8 @@ class Zone extends LinkedHashMap { .reason("OK") .headers(Collections.emptyMap()) .request( - Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + Request.create( + Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .body(zonesJson, UTF_8) .build(); @@ -104,7 +106,8 @@ void nullBodyDecodesToNull() throws Exception { .reason("OK") .headers(Collections.emptyMap()) .request( - Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + Request.create( + Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .build(); assertThat(new MoshiDecoder().decode(response, String.class)).isNull(); } @@ -117,7 +120,8 @@ void emptyBodyDecodesToNull() throws Exception { .reason("OK") .headers(Collections.emptyMap()) .request( - Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + Request.create( + Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .body(new byte[0]) .build(); assertThat(new MoshiDecoder().decode(response, String.class)).isNull(); @@ -132,7 +136,8 @@ void notFoundDecodesToEmpty() throws Exception { .reason("NOT FOUND") .headers(Collections.emptyMap()) .request( - Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + Request.create( + Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .build(); assertThat((byte[]) new MoshiDecoder().decode(response, byte[].class)).isEmpty(); } @@ -153,7 +158,8 @@ void customDecoder() throws Exception { .reason("OK") .headers(Collections.emptyMap()) .request( - Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + Request.create( + Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .body(zonesJson, UTF_8) .build(); @@ -175,7 +181,8 @@ void customObjectDecoder() throws Exception { .reason("OK") .headers(Collections.emptyMap()) .request( - Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + Request.create( + Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .body(videoGamesJson, UTF_8) .build(); diff --git a/okhttp/src/main/java/feign/okhttp/OkHttpClient.java b/okhttp/src/main/java/feign/okhttp/OkHttpClient.java index ee582ad7a4..25a8b7f1b1 100644 --- a/okhttp/src/main/java/feign/okhttp/OkHttpClient.java +++ b/okhttp/src/main/java/feign/okhttp/OkHttpClient.java @@ -29,15 +29,7 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.Headers; -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; -import okio.BufferedSink; +import okhttp3.*; /** * This module directs Feign's http requests to @@ -75,6 +67,9 @@ static Request toOkHttpRequest(feign.Request input) { requestBuilder.addHeader(field, value); if (field.equalsIgnoreCase("Content-Type")) { mediaType = MediaType.parse(value); + if (input.charset() != null) { + mediaType.charset(input.charset()); + } } } } @@ -83,40 +78,19 @@ static Request toOkHttpRequest(feign.Request input) { requestBuilder.addHeader("Accept", "*/*"); } - RequestBody body = input.httpMethod().isWithBody() ? toRequestBody(input, mediaType) : null; - requestBuilder.method(input.httpMethod().name(), body); - return requestBuilder.build(); - } - - static RequestBody toRequestBody(feign.Request request, MediaType mediaType) { - return request - .body() - .map(body -> toRequestBody(body, mediaType)) - .orElseGet(() -> RequestBody.create(new byte[0], mediaType)); - } - - static RequestBody toRequestBody(feign.Request.Body body, MediaType mediaType) { - return new RequestBody() { - @Override - public MediaType contentType() { - return mediaType; - } - - @Override - public long contentLength() { - return body.contentLength(); - } - - @Override - public boolean isOneShot() { - return !body.isRepeatable(); + byte[] inputBody = input.body(); + if (input.httpMethod().isWithBody()) { + requestBuilder.removeHeader("Content-Type"); + if (inputBody == null) { + // write an empty BODY to conform with okhttp 2.4.0+ + // http://johnfeng.github.io/blog/2015/06/30/okhttp-updates-post-wouldnt-be-allowed-to-have-null-body/ + inputBody = new byte[0]; } + } - @Override - public void writeTo(BufferedSink sink) throws IOException { - body.writeTo(sink.outputStream()); - } - }; + RequestBody body = inputBody != null ? RequestBody.create(mediaType, inputBody) : null; + requestBuilder.method(input.httpMethod().name(), body); + return requestBuilder.build(); } private static feign.Response toFeignResponse(Response response, feign.Request request) diff --git a/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java b/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java index c03eb1ef4b..79150b86b9 100644 --- a/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java +++ b/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java @@ -16,6 +16,7 @@ package feign.okhttp; import static feign.assertj.MockWebServerAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; import static org.assertj.core.data.MapEntry.entry; @@ -527,7 +528,7 @@ void throwsFeignExceptionIncludingBody() throws Throwable { } catch (final FeignException e) { assertThat(e.getMessage()) .isEqualTo("timeout reading POST http://localhost:" + server.getPort() + "/"); - assertThat(e.contentUTF8()).isEmpty(); + assertThat(e.contentUTF8()).isEqualTo("Request body"); return; } fail(""); @@ -564,7 +565,7 @@ void whenReturnTypeIsResponseNoErrorHandling() throws Throwable { .status(302) .reason("Found") .headers(headers) - .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, null)) + .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, Util.UTF_8)) .body(new byte[0]) .build(); @@ -768,7 +769,7 @@ private Response responseWithText(String text) { return Response.builder() .body(text, Util.UTF_8) .status(200) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(new HashMap<>()) .build(); } @@ -1026,9 +1027,9 @@ static final class TestInterfaceAsyncBuilder { .encoder( (object, _, template) -> { if (object instanceof Map) { - template.body(Request.Body.of(new Gson().toJson(object))); + template.body(new Gson().toJson(object)); } else { - template.body(Request.Body.of(object.toString())); + template.body(object.toString()); } }); diff --git a/pom.xml b/pom.xml index afcb4317da..3b613d5f81 100644 --- a/pom.xml +++ b/pom.xml @@ -983,14 +983,6 @@ @feign.Experimental feign.graphql - - feign.Request - - feign.RequestTemplate - - feign.mock.RequestKey - - feign.vertx.VertxHttpClient public diff --git a/ribbon/src/main/java/feign/ribbon/LBClient.java b/ribbon/src/main/java/feign/ribbon/LBClient.java index 260b57df9e..1062abb173 100644 --- a/ribbon/src/main/java/feign/ribbon/LBClient.java +++ b/ribbon/src/main/java/feign/ribbon/LBClient.java @@ -122,18 +122,15 @@ static class RibbonRequest extends ClientRequest implements Cloneable { @SuppressWarnings("deprecation") Request toRequest() { + // add header "Content-Length" according to the request body + final byte[] body = request.body(); + final int bodyLength = body != null ? body.length : 0; // create a new Map to avoid side effect, not to change the old headers Map> headers = new LinkedHashMap>(); headers.putAll(request.headers()); - String contentLength = - request.body().map(body -> String.valueOf(body.contentLength())).orElse("0"); - headers.put(Util.CONTENT_LENGTH, Collections.singletonList(contentLength)); + headers.put(Util.CONTENT_LENGTH, Collections.singletonList(String.valueOf(bodyLength))); return Request.create( - request.httpMethod(), - getUri().toASCIIString(), - headers, - request.body().orElse(null), - null); + request.httpMethod(), getUri().toASCIIString(), headers, body, request.charset()); } Client client() { diff --git a/ribbon/src/test/java/feign/ribbon/LBClientTest.java b/ribbon/src/test/java/feign/ribbon/LBClientTest.java index b2086c6744..ad29059cbe 100644 --- a/ribbon/src/test/java/feign/ribbon/LBClientTest.java +++ b/ribbon/src/test/java/feign/ribbon/LBClientTest.java @@ -22,6 +22,7 @@ import feign.ribbon.LBClient.RibbonRequest; import java.net.URI; import java.net.URISyntaxException; +import java.nio.charset.Charset; import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; @@ -47,7 +48,8 @@ void ribbonRequest() throws URISyntaxException { URI uri = new URI(urlWithEncodedJson); Map> headers = new LinkedHashMap<>(); // create a Request for recreating another Request by toRequest() - Request requestOrigin = Request.create(method, uri.toASCIIString(), headers, null, null); + Request requestOrigin = + Request.create(method, uri.toASCIIString(), headers, null, Charset.forName("utf-8")); RibbonRequest ribbonRequest = new RibbonRequest(null, requestOrigin, uri); // use toRequest() recreate a Request diff --git a/sax/src/test/java/feign/sax/SAXDecoderTest.java b/sax/src/test/java/feign/sax/SAXDecoderTest.java index 5b23f6eb1e..21fdc29890 100644 --- a/sax/src/test/java/feign/sax/SAXDecoderTest.java +++ b/sax/src/test/java/feign/sax/SAXDecoderTest.java @@ -22,6 +22,7 @@ import feign.Request; import feign.Request.HttpMethod; import feign.Response; +import feign.Util; import feign.codec.Decoder; import java.io.IOException; import java.text.ParseException; @@ -76,7 +77,7 @@ private Response statusFailedResponse() { return Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body(statusFailed, UTF_8) .build(); @@ -88,7 +89,8 @@ void nullBodyDecodesToEmpty() throws Exception { Response.builder() .status(204) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .build(); assertThat((byte[]) decoder.decode(response, byte[].class)).isEmpty(); @@ -101,7 +103,8 @@ void notFoundDecodesToEmpty() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .build(); assertThat((byte[]) decoder.decode(response, byte[].class)).isEmpty(); diff --git a/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java b/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java index bfb107a583..aac5a2ede5 100644 --- a/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java +++ b/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java @@ -16,12 +16,10 @@ package feign.sax.examples; import static feign.Util.UTF_8; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import feign.Request; import feign.RequestTemplate; import java.net.URI; -import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.time.Clock; import javax.crypto.Mac; @@ -74,7 +72,8 @@ private static String canonicalString(RequestTemplate input, String host) { canonicalRequest.append("host").append('\n'); // HexEncode(Hash(Payload)) - String bodyText = input.requestBody().map(AWSSignatureVersion4::bodyAsUtf8String).orElse(null); + byte[] data = input.body(); + String bodyText = (data != null) ? new String(data, input.requestCharset()) : null; if (bodyText != null) { canonicalRequest.append(hex(sha256(bodyText))); } else { @@ -83,10 +82,6 @@ private static String canonicalString(RequestTemplate input, String host) { return canonicalRequest.toString(); } - private static String bodyAsUtf8String(Request.Body body) { - return assertDoesNotThrow(() -> body.writeToString(StandardCharsets.UTF_8)); - } - private static String toSign(String timestamp, String credentialScope, String canonicalRequest) { StringBuilder toSign = new StringBuilder(); // Algorithm + '\n' + @@ -121,7 +116,7 @@ public Request apply(RequestTemplate input) { if (!input.headers().isEmpty()) { throw new UnsupportedOperationException("headers not supported"); } - if (input.requestBody().isPresent()) { + if (input.body() != null) { throw new UnsupportedOperationException("body not supported"); } diff --git a/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java b/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java index 6df54f9ef7..c4eaf5bfd5 100644 --- a/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java +++ b/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java @@ -42,7 +42,7 @@ public class Slf4jLoggerTest { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body(new byte[0]) .build(); @@ -125,7 +125,8 @@ void rebuffersResponseBodyWhenLogLevelIsInfo() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body("{\"error\":\"test\"}", Util.UTF_8) .build(); @@ -150,7 +151,8 @@ void rebuffersResponseBodyWhenLogLevelIsFull() throws Exception { Response.builder() .status(500) .reason("Internal Server Error") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body("{\"message\":\"error details\"}", Util.UTF_8) .build(); @@ -176,7 +178,8 @@ void responseBodyReadableMultipleTimes() throws Exception { .status(400) .reason("Bad Request") .request( - Request.create(HttpMethod.POST, "/api/submit", Collections.emptyMap(), null, null)) + Request.create( + HttpMethod.POST, "/api/submit", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.>emptyMap()) .body(originalBody, Util.UTF_8) .build(); diff --git a/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java b/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java index 31bc768304..709b7f2a13 100644 --- a/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java +++ b/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java @@ -15,7 +15,6 @@ */ package feign.soap; -import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; @@ -33,11 +32,7 @@ import java.nio.charset.StandardCharsets; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.OutputKeys; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.TransformerFactoryConfigurationError; +import javax.xml.transform.*; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.w3c.dom.Document; @@ -136,7 +131,7 @@ public void encode(Object object, Type bodyType, RequestTemplate template) { } else { soapMessage.writeTo(bos); } - template.body(Request.Body.of(bos.toByteArray())); + template.body(bos.toString()); } catch (SOAPException | JAXBException | ParserConfigurationException diff --git a/soap-jakarta/src/test/java/feign/soap/SOAPCodecTest.java b/soap-jakarta/src/test/java/feign/soap/SOAPCodecTest.java index 8634ff9815..0cbb94be42 100644 --- a/soap-jakarta/src/test/java/feign/soap/SOAPCodecTest.java +++ b/soap-jakarta/src/test/java/feign/soap/SOAPCodecTest.java @@ -17,13 +17,14 @@ import static feign.Util.UTF_8; import static feign.assertj.FeignAssertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import feign.Request; import feign.Request.HttpMethod; import feign.RequestTemplate; import feign.Response; +import feign.Util; import feign.codec.Encoder; import feign.jaxb.JAXBContextFactory; import jakarta.xml.bind.annotation.XmlAccessType; @@ -253,7 +254,8 @@ void decodesSoap() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(mockSoapEnvelop, UTF_8) .build(); @@ -288,7 +290,8 @@ void decodesSoapWithSchemaOnEnvelope() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(mockSoapEnvelop, UTF_8) .build(); @@ -325,7 +328,8 @@ void decodesSoap1_2Protocol() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(mockSoapEnvelop, UTF_8) .build(); @@ -349,7 +353,8 @@ class ParameterizedHolder { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body( """ @@ -399,9 +404,10 @@ void decodeAnnotatedParameterizedTypes() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) - .body(template.requestBody().map(this::bodyAsBytes).orElse(null)) + .body(template.body()) .build(); new SOAPDecoder(new JAXBContextFactory.Builder().build()).decode(response, Box.class); @@ -414,7 +420,8 @@ void notFoundDecodesToNull() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .build(); assertThat( @@ -459,10 +466,6 @@ void changeSoapProtocolAndSetHeader() { assertThat(template).hasBody(soapEnvelop); } - private byte[] bodyAsBytes(Request.Body body) { - return assertDoesNotThrow(body::writeToByteArray); - } - @XmlRootElement static class Box { diff --git a/soap-jakarta/src/test/java/feign/soap/SOAPFaultDecoderTest.java b/soap-jakarta/src/test/java/feign/soap/SOAPFaultDecoderTest.java index 85cd3979a5..c94f83b16a 100644 --- a/soap-jakarta/src/test/java/feign/soap/SOAPFaultDecoderTest.java +++ b/soap-jakarta/src/test/java/feign/soap/SOAPFaultDecoderTest.java @@ -23,6 +23,7 @@ import feign.Request; import feign.Request.HttpMethod; import feign.Response; +import feign.Util; import feign.jaxb.JAXBContextFactory; import jakarta.xml.soap.SOAPConstants; import jakarta.xml.ws.soap.SOAPFaultException; @@ -49,7 +50,8 @@ void soapDecoderThrowsSOAPFaultException() throws IOException { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(getResourceBytes("/samples/SOAP_1_2_FAULT.xml")) .build(); @@ -71,7 +73,8 @@ void errorDecoderReturnsSOAPFaultException() throws IOException { Response.builder() .status(400) .reason("BAD REQUEST") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(getResourceBytes("/samples/SOAP_1_1_FAULT.xml")) .build(); @@ -88,7 +91,8 @@ void errorDecoderReturnsFeignExceptionOn503Status() throws IOException { Response.builder() .status(503) .reason("Service Unavailable") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body("Service Unavailable", UTF_8) .build(); @@ -119,7 +123,8 @@ void errorDecoderReturnsFeignExceptionOnEmptyFault() throws IOException { Response.builder() .status(500) .reason("Internal Server Error") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(responseBody, UTF_8) .build(); diff --git a/soap/src/main/java/feign/soap/SOAPEncoder.java b/soap/src/main/java/feign/soap/SOAPEncoder.java index 711c44171f..24f10da917 100644 --- a/soap/src/main/java/feign/soap/SOAPEncoder.java +++ b/soap/src/main/java/feign/soap/SOAPEncoder.java @@ -15,7 +15,6 @@ */ package feign.soap; -import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; @@ -136,7 +135,7 @@ public void encode(Object object, Type bodyType, RequestTemplate template) { } else { soapMessage.writeTo(bos); } - template.body(Request.Body.of(bos.toByteArray())); + template.body(new String(bos.toByteArray())); } catch (SOAPException | JAXBException | ParserConfigurationException diff --git a/soap/src/test/java/feign/soap/SOAPCodecTest.java b/soap/src/test/java/feign/soap/SOAPCodecTest.java index 59d38432da..002a0e33b6 100644 --- a/soap/src/test/java/feign/soap/SOAPCodecTest.java +++ b/soap/src/test/java/feign/soap/SOAPCodecTest.java @@ -17,13 +17,14 @@ import static feign.Util.UTF_8; import static feign.assertj.FeignAssertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import feign.Request; import feign.Request.HttpMethod; import feign.RequestTemplate; import feign.Response; +import feign.Util; import feign.codec.Encoder; import feign.jaxb.JAXBContextFactory; import java.lang.reflect.Type; @@ -253,7 +254,8 @@ void decodesSoap() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(mockSoapEnvelop, UTF_8) .build(); @@ -288,7 +290,8 @@ void decodesSoapWithSchemaOnEnvelope() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(mockSoapEnvelop, UTF_8) .build(); @@ -325,7 +328,8 @@ void decodesSoap1_2Protocol() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(mockSoapEnvelop, UTF_8) .build(); @@ -349,7 +353,8 @@ class ParameterizedHolder { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body( """ @@ -409,9 +414,10 @@ void decodeAnnotatedParameterizedTypes() throws Exception { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) - .body(template.requestBody().map(this::bodyAsBytes).orElse(null)) + .body(template.body()) .build(); new SOAPDecoder(new JAXBContextFactory.Builder().build()).decode(response, Box.class); @@ -424,7 +430,8 @@ void notFoundDecodesToNull() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .build(); assertThat( @@ -469,10 +476,6 @@ void changeSoapProtocolAndSetHeader() { assertThat(template).hasBody(soapEnvelop); } - private byte[] bodyAsBytes(Request.Body body) { - return assertDoesNotThrow(body::writeToByteArray); - } - static class ChangedProtocolAndHeaderSOAPEncoder extends SOAPEncoder { public ChangedProtocolAndHeaderSOAPEncoder(JAXBContextFactory jaxbContextFactory) { diff --git a/soap/src/test/java/feign/soap/SOAPFaultDecoderTest.java b/soap/src/test/java/feign/soap/SOAPFaultDecoderTest.java index 810f173f11..15d3f76696 100644 --- a/soap/src/test/java/feign/soap/SOAPFaultDecoderTest.java +++ b/soap/src/test/java/feign/soap/SOAPFaultDecoderTest.java @@ -23,6 +23,7 @@ import feign.Request; import feign.Request.HttpMethod; import feign.Response; +import feign.Util; import feign.jaxb.JAXBContextFactory; import java.io.DataInputStream; import java.io.IOException; @@ -42,7 +43,8 @@ void soapDecoderThrowsSOAPFaultException() throws IOException { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(getResourceBytes("/samples/SOAP_1_2_FAULT.xml")) .build(); @@ -63,7 +65,8 @@ void errorDecoderReturnsSOAPFaultException() throws IOException { Response.builder() .status(400) .reason("BAD REQUEST") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(getResourceBytes("/samples/SOAP_1_1_FAULT.xml")) .build(); @@ -80,7 +83,8 @@ void errorDecoderReturnsFeignExceptionOn503Status() throws IOException { Response.builder() .status(503) .reason("Service Unavailable") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body("Service Unavailable", UTF_8) .build(); @@ -111,7 +115,8 @@ void errorDecoderReturnsFeignExceptionOnEmptyFault() throws IOException { Response.builder() .status(500) .reason("Internal Server Error") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(responseBody, UTF_8) .build(); diff --git a/spring/src/test/java/feign/spring/SpringContractTest.java b/spring/src/test/java/feign/spring/SpringContractTest.java index 7788815757..4f0277d70e 100755 --- a/spring/src/test/java/feign/spring/SpringContractTest.java +++ b/spring/src/test/java/feign/spring/SpringContractTest.java @@ -16,7 +16,6 @@ package feign.spring; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; import feign.Feign; @@ -182,8 +181,7 @@ void nonRequiredRequestBodyIsNull() { resource.checkWithNonRequiredRequestBody(null); Request request = mockClient.verifyOne(HttpMethod.POST, "/health/withNonRequiredRequestBody"); - assertThat(request.requestTemplate().requestBody().map(this::bodyAsUtf8String)) - .contains("null"); + assertThat(request.requestTemplate().body()).asString().isEqualTo("null"); } @Test @@ -193,8 +191,7 @@ void nonRequiredRequestBodyIsObject() { resource.checkWithNonRequiredRequestBody(object); Request request = mockClient.verifyOne(HttpMethod.POST, "/health/withNonRequiredRequestBody"); - assertThat(request.requestTemplate().requestBody().map(this::bodyAsUtf8String).orElse(null)) - .contains("\"name\" : \"hello\""); + assertThat(request.requestTemplate().body()).asString().contains("\"name\" : \"hello\""); } @Test @@ -284,10 +281,6 @@ void consumeAndProduce() { assertThat(request.headers()).containsEntry("Accept", Arrays.asList("text/plain")); } - private String bodyAsUtf8String(Request.Body body) { - return assertDoesNotThrow(() -> body.writeToString(StandardCharsets.UTF_8)); - } - interface GenericResource { @RequestMapping(value = "generic", method = RequestMethod.GET) diff --git a/validation-jakarta/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java b/validation-jakarta/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java index 36748ac295..b2c977a242 100644 --- a/validation-jakarta/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java +++ b/validation-jakarta/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java @@ -20,7 +20,6 @@ import feign.Feign; import feign.Param; -import feign.Request; import feign.RequestLine; import jakarta.validation.ConstraintViolationException; import jakarta.validation.Valid; @@ -77,8 +76,7 @@ interface Api { private Api api() { return Feign.builder() - .encoder( - (object, bodyType, template) -> template.body(Request.Body.of(String.valueOf(object)))) + .encoder((object, bodyType, template) -> template.body(String.valueOf(object))) .methodInterceptor(BeanValidationMethodInterceptor.usingDefaultFactory()) .target(Api.class, "http://localhost:" + server.getPort()); } diff --git a/validation/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java b/validation/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java index a25b393a38..11c19dd111 100644 --- a/validation/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java +++ b/validation/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java @@ -20,7 +20,6 @@ import feign.Feign; import feign.Param; -import feign.Request; import feign.RequestLine; import javax.validation.ConstraintViolationException; import javax.validation.Valid; @@ -77,8 +76,7 @@ interface Api { private Api api() { return Feign.builder() - .encoder( - (object, bodyType, template) -> template.body(Request.Body.of(String.valueOf(object)))) + .encoder((object, bodyType, template) -> template.body(String.valueOf(object))) .methodInterceptor(BeanValidationMethodInterceptor.usingDefaultFactory()) .target(Api.class, "http://localhost:" + server.getPort()); } diff --git a/vertx/feign-vertx/src/main/java/feign/VertxFeign.java b/vertx/feign-vertx/src/main/java/feign/VertxFeign.java index 24a24cef59..09a6264ebe 100644 --- a/vertx/feign-vertx/src/main/java/feign/VertxFeign.java +++ b/vertx/feign-vertx/src/main/java/feign/VertxFeign.java @@ -28,7 +28,6 @@ import feign.querymap.FieldQueryMapEncoder; import feign.vertx.VertxDelegatingContract; import feign.vertx.VertxHttpClient; -import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.ext.web.client.HttpRequest; import io.vertx.ext.web.client.WebClient; @@ -95,7 +94,6 @@ public T newInstance(final Target target) { /** VertxFeign builder. */ public static final class Builder extends Feign.Builder { - private Vertx vertx; private WebClient webClient; private final List requestInterceptors = new ArrayList<>(); private Logger.Level logLevel = Logger.Level.NONE; @@ -124,11 +122,6 @@ public Builder invocationHandlerFactory( throw new UnsupportedOperationException(); } - public Builder vertx(final Vertx vertx) { - this.vertx = vertx; - return this; - } - /** * Sets a vertx WebClient. * @@ -377,12 +370,10 @@ public Feign.Builder options(final Request.Options options) { @Override public VertxFeign internalBuild() { - checkNotNull(this.vertx, "Vertx instance wasn't provided in VertxFeign builder"); checkNotNull( this.webClient, "Vertx WebClient instance wasn't provided in VertxFeign builder"); - final VertxHttpClient client = - new VertxHttpClient(vertx, webClient, timeout, requestPreProcessor); + final VertxHttpClient client = new VertxHttpClient(webClient, timeout, requestPreProcessor); final VertxMethodHandler.Factory methodHandlerFactory = new VertxMethodHandler.Factory( client, retryer, requestInterceptors, logger, logLevel, decode404); diff --git a/vertx/feign-vertx/src/main/java/feign/vertx/OutputToReadStream.java b/vertx/feign-vertx/src/main/java/feign/vertx/OutputToReadStream.java deleted file mode 100644 index e124976ad7..0000000000 --- a/vertx/feign-vertx/src/main/java/feign/vertx/OutputToReadStream.java +++ /dev/null @@ -1,308 +0,0 @@ -/* - * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package feign.vertx; - -import io.vertx.core.AsyncResult; -import io.vertx.core.Context; -import io.vertx.core.Future; -import io.vertx.core.Handler; -import io.vertx.core.Promise; -import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.streams.ReadStream; -import io.vertx.core.streams.WriteStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Objects; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; - -/** - * A conversion utility to help move data from a Java classic blocking IO to a Vert.x asynchronous - * stream. - * - *

Adapted from {@code io.cloudonix.vertx.javaio.OutputToReadStream} in {@code - * io.cloudonix:vertx-java.io:5.0.8} (MIT License), with local changes for Feign. - * - *

This class is copied here to keep compatibility with both Vert.x 4 and Vert.x 5. - * - *

Main compatibility change: {@link #pipeFromInput(InputStream, WriteStream)} uses {@code - * Future.onComplete(...)} rather than {@code Future.andThen(...)} to avoid a runtime linkage to - * {@code io.vertx.core.Completable}, which does not exist in Vert.x 4. - * - *

Use this class to create an {@link OutputStream} that pushes data written to it to a {@link - * ReadStream} API. - * - *

The ReadStream handlers are called on a Vert.x context, and {@link #close()} must be called - * for the ReadStream end handler to be triggered. - * - *

It is recommended to use this class in a blocking try-with-resources block, to ensure that - * streams are closed properly. For example: - * - *

{@code try (final OutputToReadStream os = new OutputToReadStream(vertx); final InputStream is - * = getInput()) { os.pipeTo(someWriteStream); is.transferTo(os); } } - * - * @author guss77 - */ -public class OutputToReadStream extends OutputStream implements ReadStream { - - private AtomicReference paused = new AtomicReference<>(new CountDownLatch(0)); - private boolean closed; - private AtomicLong demand = new AtomicLong(0); - private Handler endHandler = v -> {}; - private Handler dataHandler = d -> {}; - private Handler errorHandler = t -> {}; - private Context context; - - public OutputToReadStream(Vertx vertx) { - context = vertx.getOrCreateContext(); - } - - /** - * Helper utility to pipe a Java {@link InputStream} to a {@link WriteStream}. - * - *

This method is non-blocking and Vert.x context safe. It uses the common ForkJoinPool to - * perform the Java blocking IO and will try to propagate IO failures to the returned {@link - * Future}. - * - *

Compatibility note: this implementation intentionally uses {@code - * onComplete} (not {@code andThen}) so it works on Vert.x 4 and Vert.x 5. - * - *

This method uses {@link InputStream#transferTo(OutputStream)} to copy all the data, and will - * then attempt to close both streams asynchronously. Some Java compilers might not detect that - * the streams will be safely closed and will issue leak warnings. - * - * @param source InputStream to drain - * @param sink WriteStream to pipe data to - * @return a Future that will succeed when all the data have been written and the streams closed, - * or fail if an {@link IOException} has occurred - */ - public Future pipeFromInput(InputStream source, WriteStream sink) { - Promise promise = Promise.promise(); - pipeTo(sink) - .onComplete( - ar -> { - if (ar.succeeded()) { - promise.complete(ar.result()); - } else { - promise.fail(ar.cause()); - } - }); - ForkJoinPool.commonPool() - .submit( - () -> { - try (final InputStream is = source; - final OutputStream os = this) { - source.transferTo(this); - } catch (IOException e) { - promise.tryFail(e); - } - }); - return promise.future(); - } - - /** - * Helper utility to pipe a Java {@link InputStream} to a {@link WriteStream}. - * - *

This method is non-blocking and Vert.x context safe. It uses the common ForkJoinPool to - * perform the Java blocking IO and will try to propagate IO failures to the returned {@link - * Future} - * - *

This method uses {@link InputStream#transferTo(OutputStream)} to copy all the data, and will - * then attempt to close both streams asynchronously. Some Java compilers might not detect that - * the streams will be safely closed and will issue leak warnings. - * - * @param source InputStream to drain - * @param sink WriteStream to pipe data to - * @param handler a handler that will be called when all the data have been written and the - * streams closed, or if an {@link IOException} has occurred. - */ - public void pipeFromInput( - InputStream source, WriteStream sink, Handler> handler) { - pipeFromInput(source, sink).onComplete(handler); - } - - /** - * Propagate an out-of-band error (likely generated or handled by the code that feeds the output - * stream) to the end of the read stream to let them know that the result is not going to be good. - * - * @param t error to be propagated down the stream - */ - public void sendError(Throwable t) { - context.executeBlocking( - () -> { - errorHandler.handle(t); - return null; - }); - } - - /* ReadStream stuff */ - - /** - * {@inheritDoc} - * - * @param handler {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public OutputToReadStream exceptionHandler(Handler handler) { - // we are usually not propagating exceptions as OutputStream has no mechanism for propagating - // exceptions down, - // except when wrapping an input stream, in which case we can forward InputStream read errors to - // the error handler. - errorHandler = Objects.requireNonNullElse(handler, t -> {}); - return this; - } - - /** - * {@inheritDoc} - * - * @param handler {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public OutputToReadStream handler(Handler handler) { - this.dataHandler = Objects.requireNonNullElse(handler, d -> {}); - return this; - } - - /** - * {@inheritDoc} - * - * @return {@inheritDoc} - */ - @Override - public OutputToReadStream pause() { - paused.getAndSet(new CountDownLatch(1)).countDown(); - return this; - } - - /** - * {@inheritDoc} - * - * @return {@inheritDoc} - */ - @Override - public OutputToReadStream resume() { - paused.getAndSet(new CountDownLatch(0)).countDown(); - return this; - } - - /** - * {@inheritDoc} - * - * @param amount {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public OutputToReadStream fetch(long amount) { - resume(); - demand.addAndGet(amount); - return null; - } - - /** - * {@inheritDoc} - * - * @param endHandler {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public OutputToReadStream endHandler(Handler endHandler) { - this.endHandler = Objects.requireNonNullElse(endHandler, v -> {}); - return this; - } - - /* OutputStream stuff */ - - /** - * {@inheritDoc} - * - * @param b {@inheritDoc} - * @throws IOException {@inheritDoc} - */ - @Override - public synchronized void write(int b) throws IOException { - if (closed) throw new IOException("OutputStream is closed"); - try { - paused.get().await(); - } catch (InterruptedException e) { - throw new IOException("Interrupted a wait for stream to resume", e); - } - push(Buffer.buffer(1).appendByte((byte) (b & 0xFF))); - } - - /** - * {@inheritDoc} - * - * @param b {@inheritDoc} - * @param off {@inheritDoc} - * @param len {@inheritDoc} - * @throws IOException {@inheritDoc} - */ - @Override - public synchronized void write(byte[] b, int off, int len) throws IOException { - if (closed) throw new IOException("OutputStream is closed"); - try { - paused.get().await(); - } catch (InterruptedException e) { - throw new IOException("Interrupted a wait for stream to resume", e); - } - push(Buffer.buffer(len - off).appendBytes(b, off, len)); - } - - /** - * {@inheritDoc} - * - * @throws IOException {@inheritDoc} - */ - @Override - public synchronized void close() throws IOException { - if (closed) return; - closed = true; - try { - paused.get().await(); - } catch (InterruptedException e) { - throw new IOException("Interrupted a wait for stream to resume", e); - } - push(null); - } - - /* Internal implementation */ - - private void push(Buffer data) { - var awaiter = new CountDownLatch(1); - context.runOnContext( - v -> { - try { - if (data == null) // end of stream - endHandler.handle(null); - else dataHandler.handle(data); - } catch (Throwable t) { - errorHandler.handle(t); - } finally { - awaiter.countDown(); - } - }); - try { - awaiter.await(); - } catch (InterruptedException e) { - } - } -} diff --git a/vertx/feign-vertx/src/main/java/feign/vertx/VertxHttpClient.java b/vertx/feign-vertx/src/main/java/feign/vertx/VertxHttpClient.java index 0a00bd2adb..0b196b6ffb 100644 --- a/vertx/feign-vertx/src/main/java/feign/vertx/VertxHttpClient.java +++ b/vertx/feign-vertx/src/main/java/feign/vertx/VertxHttpClient.java @@ -44,7 +44,6 @@ */ @SuppressWarnings("unused") public final class VertxHttpClient { - private final Vertx vertx; private final WebClient webClient; private final long timeout; private final UnaryOperator> requestPreProcessor; @@ -57,15 +56,12 @@ public final class VertxHttpClient { * @param requestPreProcessor request pre-processor */ public VertxHttpClient( - final Vertx vertx, final WebClient webClient, final long timeout, final UnaryOperator> requestPreProcessor) { - checkNotNull(vertx, "Argument vertx must not be null"); checkNotNull(webClient, "Argument webClient must not be null"); checkNotNull(requestPreProcessor, "Argument requestPreProcessor must be not null"); - this.vertx = vertx; this.webClient = webClient; this.timeout = timeout; this.requestPreProcessor = requestPreProcessor; @@ -89,25 +85,9 @@ public Future execute(final Request request) { } final Future> responseFuture = - request - .body() - .map( - body -> { - OutputToReadStream stream = new OutputToReadStream(vertx); - Future> sendStreamFuture = - httpClientRequest.sendStream(stream); - Future writeFuture = - vertx.executeBlocking( - () -> { - try (stream) { - body.writeTo(stream); - } - return null; - }); - return Future.all(sendStreamFuture, writeFuture) - .map(composite -> sendStreamFuture.result()); - }) - .orElseGet(httpClientRequest::send); + request.body() != null + ? httpClientRequest.sendBuffer(Buffer.buffer(request.body())) + : httpClientRequest.send(); return responseFuture.compose( response -> { diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java b/vertx/feign-vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java index dd7f496b6b..5931c7acb4 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java @@ -81,7 +81,6 @@ void http11NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { HelloServiceAPI client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) @@ -109,7 +108,6 @@ void http2NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { HelloServiceAPI client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java index 88dab3713b..fae9a70e9d 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java @@ -38,7 +38,6 @@ protected void createClient(final Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/QueryMapEncoderTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/QueryMapEncoderTest.java index 69e3d5dfd8..39a43a8adb 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/QueryMapEncoderTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/QueryMapEncoderTest.java @@ -55,7 +55,6 @@ void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .queryMapEncoder(new CustomQueryMapEncoder()) diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/RawContractTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/RawContractTest.java index e816622474..48f46b4b49 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/RawContractTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/RawContractTest.java @@ -45,7 +45,6 @@ class RawContractTest extends AbstractFeignVertxTest { static void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(WebClient.create(vertx)) .encoder(new JacksonEncoder(TestUtils.MAPPER)) .decoder(new JacksonDecoder(TestUtils.MAPPER)) diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/RequestPreProcessorTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/RequestPreProcessorTest.java index 2d88afa6c9..87850cb9e3 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/RequestPreProcessorTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/RequestPreProcessorTest.java @@ -47,7 +47,6 @@ void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .requestPreProcessor(req -> req.addQueryParam("version", "v1")) diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/RetryingTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/RetryingTest.java index 89691c9d5a..b294d99363 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/RetryingTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/RetryingTest.java @@ -48,7 +48,6 @@ class RetryingTest extends AbstractFeignVertxTest { static void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(WebClient.create(vertx)) .decoder(new JacksonDecoder(MAPPER)) .retryer(new DefaultRetryer(100, SECONDS.toMillis(1), 5)) diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/TimeoutHandlingTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/TimeoutHandlingTest.java index 5bd97dd4a5..29e7efa6e1 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/TimeoutHandlingTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/TimeoutHandlingTest.java @@ -48,7 +48,6 @@ void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .timeout(1000) diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpClientTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpClientTest.java index 005568436b..eca7765385 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpClientTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpClientTest.java @@ -65,7 +65,6 @@ void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .logger(new Slf4jLogger()) @@ -218,7 +217,6 @@ class WhenMakePostRequest { void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(WebClient.create(vertx)) .encoder(new JacksonEncoder(TestUtils.MAPPER)) .decoder(new JacksonDecoder(TestUtils.MAPPER)) @@ -306,23 +304,6 @@ void whenVertxMissing() { ThrowableAssert.ThrowingCallable instantiateContractForgottenVertx = () -> VertxFeign.builder().target(IcecreamServiceApi.class, wireMock.baseUrl()); - /* Then */ - assertThatCode(instantiateContractForgottenVertx) - .isInstanceOf(NullPointerException.class) - .hasMessage("Vertx instance wasn't provided in VertxFeign builder"); - } - - @Test - @DisplayName("when Vertx WebClient is not provided") - void whenVertxWebClientMissing(Vertx vertx) { - - /* Given */ - ThrowableAssert.ThrowingCallable instantiateContractForgottenVertx = - () -> - VertxFeign.builder() - .vertx(vertx) - .target(IcecreamServiceApi.class, wireMock.baseUrl()); - /* Then */ assertThatCode(instantiateContractForgottenVertx) .isInstanceOf(NullPointerException.class) @@ -337,7 +318,6 @@ void whenTryToInstantiateBrokenContract(Vertx vertx) { ThrowableAssert.ThrowingCallable instantiateBrokenContract = () -> VertxFeign.builder() - .vertx(vertx) .webClient(WebClient.create(vertx)) .target(IcecreamServiceApiBroken.class, wireMock.baseUrl()); diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpOptionsTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpOptionsTest.java index 21eefbe9ca..626376a347 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpOptionsTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpOptionsTest.java @@ -65,7 +65,6 @@ void httpClientOptions(Vertx vertx, VertxTestContext testContext) { IcecreamServiceApi client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .logger(new Slf4jLogger()) @@ -87,7 +86,6 @@ void requestOptions(Vertx vertx, VertxTestContext testContext) { IcecreamServiceApi client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .logger(new Slf4jLogger()) diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/ConnectionsLeakTests.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/ConnectionsLeakTests.java index 4dcde50338..b76b751ed0 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/ConnectionsLeakTests.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/ConnectionsLeakTests.java @@ -84,7 +84,6 @@ void http11NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { HelloServiceAPI client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) @@ -112,7 +111,6 @@ void http2NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { HelloServiceAPI client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http11ClientReconnectTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http11ClientReconnectTest.java index bb6edaf4a8..207ae8a89e 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http11ClientReconnectTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http11ClientReconnectTest.java @@ -36,7 +36,6 @@ protected void createClient(final Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/QueryMapEncoderTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/QueryMapEncoderTest.java index 69e3d5dfd8..39a43a8adb 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/QueryMapEncoderTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/QueryMapEncoderTest.java @@ -55,7 +55,6 @@ void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .queryMapEncoder(new CustomQueryMapEncoder()) diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/RawContractTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/RawContractTest.java index e816622474..48f46b4b49 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/RawContractTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/RawContractTest.java @@ -45,7 +45,6 @@ class RawContractTest extends AbstractFeignVertxTest { static void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(WebClient.create(vertx)) .encoder(new JacksonEncoder(TestUtils.MAPPER)) .decoder(new JacksonDecoder(TestUtils.MAPPER)) diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/RequestPreProcessorTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/RequestPreProcessorTest.java index 2d88afa6c9..87850cb9e3 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/RequestPreProcessorTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/RequestPreProcessorTest.java @@ -47,7 +47,6 @@ void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .requestPreProcessor(req -> req.addQueryParam("version", "v1")) diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/RetryingTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/RetryingTest.java index 89691c9d5a..b294d99363 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/RetryingTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/RetryingTest.java @@ -48,7 +48,6 @@ class RetryingTest extends AbstractFeignVertxTest { static void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(WebClient.create(vertx)) .decoder(new JacksonDecoder(MAPPER)) .retryer(new DefaultRetryer(100, SECONDS.toMillis(1), 5)) diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/TimeoutHandlingTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/TimeoutHandlingTest.java index 5bd97dd4a5..29e7efa6e1 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/TimeoutHandlingTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/TimeoutHandlingTest.java @@ -48,7 +48,6 @@ void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .timeout(1000) diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpClientTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpClientTest.java index 005568436b..eca7765385 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpClientTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpClientTest.java @@ -65,7 +65,6 @@ void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .logger(new Slf4jLogger()) @@ -218,7 +217,6 @@ class WhenMakePostRequest { void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(WebClient.create(vertx)) .encoder(new JacksonEncoder(TestUtils.MAPPER)) .decoder(new JacksonDecoder(TestUtils.MAPPER)) @@ -306,23 +304,6 @@ void whenVertxMissing() { ThrowableAssert.ThrowingCallable instantiateContractForgottenVertx = () -> VertxFeign.builder().target(IcecreamServiceApi.class, wireMock.baseUrl()); - /* Then */ - assertThatCode(instantiateContractForgottenVertx) - .isInstanceOf(NullPointerException.class) - .hasMessage("Vertx instance wasn't provided in VertxFeign builder"); - } - - @Test - @DisplayName("when Vertx WebClient is not provided") - void whenVertxWebClientMissing(Vertx vertx) { - - /* Given */ - ThrowableAssert.ThrowingCallable instantiateContractForgottenVertx = - () -> - VertxFeign.builder() - .vertx(vertx) - .target(IcecreamServiceApi.class, wireMock.baseUrl()); - /* Then */ assertThatCode(instantiateContractForgottenVertx) .isInstanceOf(NullPointerException.class) @@ -337,7 +318,6 @@ void whenTryToInstantiateBrokenContract(Vertx vertx) { ThrowableAssert.ThrowingCallable instantiateBrokenContract = () -> VertxFeign.builder() - .vertx(vertx) .webClient(WebClient.create(vertx)) .target(IcecreamServiceApiBroken.class, wireMock.baseUrl()); diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpOptionsTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpOptionsTest.java index 2ca7a54cb6..0e83d3ea62 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpOptionsTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpOptionsTest.java @@ -64,7 +64,6 @@ void httpClientOptions(Vertx vertx, VertxTestContext testContext) { IcecreamServiceApi client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .logger(new Slf4jLogger()) @@ -86,7 +85,6 @@ void requestOptions(Vertx vertx, VertxTestContext testContext) { IcecreamServiceApi client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .logger(new Slf4jLogger())