From ef5d3b90c48029db9edbe23c4f904b9869623684 Mon Sep 17 00:00:00 2001 From: Yevhen Vasyliev Date: Mon, 18 May 2026 07:14:59 +0300 Subject: [PATCH 1/7] feat!: make `feign.Request.Body` streaming-ready Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com> --- .../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 | 263 +++++++-------- core/src/main/java/feign/RequestTemplate.java | 58 +--- .../main/java/feign/codec/DefaultEncoder.java | 5 +- .../java/feign/utils/ThrowingConsumer.java | 34 ++ .../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 +- .../test/java/feign/LoggerMethodsTest.java | 2 +- .../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 | 100 ++++-- .../main/java/feign/hc5/FeignBodyEntity.java | 97 ++++++ .../feign/hc5/AsyncApacheHttp5ClientTest.java | 11 +- .../feign/httpclient/ApacheHttpClient.java | 102 +++++- .../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 | 36 +- .../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 +- .../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 | 3 +- .../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 +- .../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 + 115 files changed, 1456 insertions(+), 857 deletions(-) create mode 100644 core/src/main/java/feign/utils/ThrowingConsumer.java create mode 100644 hc5/src/main/java/feign/hc5/FeignBodyEntity.java create mode 100644 vertx/feign-vertx/src/main/java/feign/vertx/OutputToReadStream.java 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 2d2c0fe74b..23d5f317b4 100644 --- a/annotation-error-decoder/src/main/java/feign/error/ExceptionGenerator.java +++ b/annotation-error-decoder/src/main/java/feign/error/ExceptionGenerator.java @@ -45,9 +45,7 @@ class ExceptionGenerator { .status(500) .body((Response.Body) null) .headers(testHeaders) - .request( - Request.create( - Request.HttpMethod.GET, "http://test", testHeaders, Request.Body.empty(), null)) + .request(Request.create(Request.HttpMethod.GET, "http://test", testHeaders, null, 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 e66a25a9f4..1d7375b30e 100644 --- a/annotation-error-decoder/src/test/java/feign/error/AbstractAnnotationErrorDecoderTest.java +++ b/annotation-error-decoder/src/test/java/feign/error/AbstractAnnotationErrorDecoderTest.java @@ -45,9 +45,7 @@ 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, Request.Body.empty(), null)) + .request(Request.create(Request.HttpMethod.GET, "http://test", headers, null, 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 9728a46f5e..82f0d9231c 100644 --- a/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderExceptionConstructorsTest.java +++ b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderExceptionConstructorsTest.java @@ -17,7 +17,6 @@ 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; @@ -56,11 +55,7 @@ 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(), - Request.Body.empty(), - null); + feign.Request.HttpMethod.GET, "http://test", Collections.emptyMap(), null, 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 76fbc1e1b3..ebe9ad7188 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, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, null)) .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 d8e02fb4ad..ec0df4644b 100644 --- a/core/src/main/java/feign/DefaultClient.java +++ b/core/src/main/java/feign/DefaultClient.java @@ -32,6 +32,7 @@ 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; @@ -187,9 +188,9 @@ else if (field.equals(ACCEPT_ENCODING)) { connection.addRequestProperty("Accept", "*/*"); } - byte[] body = request.body(); + Optional body = request.body(); - if (body != null) { + if (body.isPresent()) { /* * Ignore disableRequestBuffering flag if the empty body was set, to ensure that internal * retry logic applies to such requests. @@ -209,7 +210,7 @@ else if (field.equals(ACCEPT_ENCODING)) { out = new DeflaterOutputStream(out); } try { - out.write(body); + body.get().writeTo(out); } finally { try { out.close(); @@ -218,7 +219,7 @@ else if (field.equals(ACCEPT_ENCODING)) { } } - if (body == null && request.httpMethod().isWithBody()) { + if (!body.isPresent() && 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 79edadde82..a59b1183c9 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(body); + data.template().body(Request.Body.of(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 b7ea794cae..f053591c96 100644 --- a/core/src/main/java/feign/FeignException.java +++ b/core/src/main/java/feign/FeignException.java @@ -15,7 +15,9 @@ */ package feign; -import static feign.Util.*; +import static feign.Util.UTF_8; +import static feign.Util.caseInsensitiveCopyOf; +import static feign.Util.checkNotNull; import static java.lang.String.format; import static java.util.regex.Pattern.CASE_INSENSITIVE; @@ -182,7 +184,7 @@ static FeignException errorReading(Request request, Response response, IOExcepti format("%s reading %s %s", cause.getMessage(), request.httpMethod(), request.url()), request, cause, - request.body(), + null, request.headers()); } diff --git a/core/src/main/java/feign/Logger.java b/core/src/main/java/feign/Logger.java index 4dcb5f0d48..b3b9e7192d 100644 --- a/core/src/main/java/feign/Logger.java +++ b/core/src/main/java/feign/Logger.java @@ -78,16 +78,18 @@ protected void logRequest(String configKey, Level logLevel, Request request) { } } - 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"); - } - } + long bodyLength = + request + .body() + .map( + body -> { + if (logLevel.ordinal() >= Level.FULL.ordinal()) { + log(configKey, ""); // CRLF + log(configKey, body.toString()); + } + return body.contentLength(); + }) + .orElse(0L); 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 a3627a164d..715c566c73 100644 --- a/core/src/main/java/feign/Request.java +++ b/core/src/main/java/feign/Request.java @@ -19,21 +19,25 @@ import static feign.Util.getThreadIdentifier; import static feign.Util.valuesOrEmpty; -import java.io.Serializable; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; 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 final class Request { public enum HttpMethod { GET, @@ -83,64 +87,6 @@ public String toString() { } } - /** - * 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. * @@ -189,17 +135,6 @@ public static Request create( protocolVersion = ProtocolVersion.HTTP_1_1; } - /** - * Http Method for this request. - * - * @return the HttpMethod string - * @deprecated @see {@link #httpMethod()} - */ - @Deprecated - public String method() { - return httpMethod.name(); - } - /** * Http Method for the request. * @@ -248,35 +183,12 @@ public void header(String key, Collection values) { } /** - * Charset of the request. + * Returns the body of the request, if any. * - * @return the current character set for the request, may be {@literal null} for binary data. + * @return the body of the request, if any */ - public Charset charset() { - return body.encoding; - } - - /** - * If present, this is the replayable body to send to the server. In some cases, this may be - * interpretable as text. - * - * @see #charset() - */ - public byte[] body() { - return body.data; - } - - public boolean isBinary() { - return body.isBinary(); - } - - /** - * Request Length. - * - * @return size of the request body. - */ - public int length() { - return this.body.length(); + public Optional body() { + return Optional.ofNullable(body); } /** @@ -309,7 +221,7 @@ public String toString() { } } if (body != null) { - builder.append('\n').append(body.asString()); + builder.append('\n').append(body); } return builder.toString(); } @@ -513,78 +425,133 @@ public RequestTemplate requestTemplate() { *

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 Body() { - super(); + public interface Body { + /** + * Creates a new {@link Body} instance from the provided string content. It's assumed that the + * content was constructed using {@link StandardCharsets#UTF_8} encoding. + * + * @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 + */ + static Body of(String content) { + return of(content, StandardCharsets.UTF_8); } - private Body(byte[] data) { - this.data = data; + /** + * 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 + */ + static Body of(byte[] content) { + return of(content, StandardCharsets.UTF_8); } - private Body(byte[] data, Charset encoding) { - this.data = data; - this.encoding = encoding; - } + /** + * 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"); - public Optional getEncoding() { - return Optional.ofNullable(this.encoding); + return of(content.getBytes(charset), charset); } - public int length() { - /* calculate the content length based on the data provided */ - return data != null ? data.length : 0; + /** + * 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); } - public byte[] asBytes() { - return data; - } + /** + * 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; - public String asString() { - return !isBinary() ? new String(data, encoding) : "Binary data"; + /** + * 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); } - public boolean isBinary() { - return encoding == null || data == null; + /** + * 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 Body create(String data) { - return new Body(data.getBytes()); - } + /** + * 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. + * + * @return {@code true} if the body can be written multiple times, {@code false} otherwise + */ + boolean isRepeatable(); - public static Body create(String data, Charset charset) { - return new Body(data.getBytes(charset), charset); + /** + * 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. + * + * @return the content length, or {@code -1} if unknown + */ + long contentLength(); + } + + private static class BodyImpl implements Body { + private final byte[] content; + private final Charset charset; + + 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"); } - public static Body create(byte[] data) { - return new Body(data); + @Override + public void writeTo(OutputStream outputStream) throws IOException { + Objects.requireNonNull(outputStream, "outputStream is required").write(content); } - public static Body create(byte[] data, Charset charset) { - return new Body(data, charset); + @Override + public boolean isRepeatable() { + return true; } - /** - * 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); + @Override + public long contentLength() { + return content.length; } - public static Body empty() { - return new Body(); + @Override + public String toString() { + return new String(content, charset); } } } diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java index 5ac205e6dd..3f3c05f844 100644 --- a/core/src/main/java/feign/RequestTemplate.java +++ b/core/src/main/java/feign/RequestTemplate.java @@ -38,6 +38,7 @@ 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; @@ -62,7 +63,7 @@ public final class RequestTemplate implements Serializable { private BodyTemplate bodyTemplate; private HttpMethod method; private transient Charset charset = Util.UTF_8; - private Request.Body body = Request.Body.empty(); + private Request.Body body; private boolean decodeSlash = true; private CollectionFormat collectionFormat = CollectionFormat.EXPLODED; private MethodMetadata methodMetadata; @@ -253,7 +254,7 @@ public RequestTemplate resolve(Map variables) { } if (this.bodyTemplate != null) { - resolved.body(this.bodyTemplate.expand(variables)); + resolved.body(Request.Body.of(this.bodyTemplate.expand(variables))); } /* mark the new template resolved */ @@ -865,22 +866,14 @@ public Map> headers() { * Sets the Body and Charset for this request. * * @param data to send, can be null. - * @param charset of the encoded data. + * @param ignoredCharset 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. */ - 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)); + @Deprecated + public RequestTemplate body(byte[] data, Charset ignoredCharset) { + this.body(Request.Body.of(data)); return this; } @@ -889,53 +882,28 @@ public RequestTemplate body(String bodyText) { * * @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; - header(CONTENT_LENGTH, Collections.emptyList()); - if (body.length() > 0) { - header(CONTENT_LENGTH, String.valueOf(body.length())); + if (body.contentLength() >= 0) { + this.header(CONTENT_LENGTH, String.valueOf(body.contentLength())); } 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. */ - @Deprecated - public Request.Body requestBody() { - return this.body; + public Optional requestBody() { + return Optional.ofNullable(this.body); } /** diff --git a/core/src/main/java/feign/codec/DefaultEncoder.java b/core/src/main/java/feign/codec/DefaultEncoder.java index 39a0f8daba..a32cf2065a 100644 --- a/core/src/main/java/feign/codec/DefaultEncoder.java +++ b/core/src/main/java/feign/codec/DefaultEncoder.java @@ -17,6 +17,7 @@ import static java.lang.String.format; +import feign.Request; import feign.RequestTemplate; import java.lang.reflect.Type; @@ -25,9 +26,9 @@ public class DefaultEncoder implements Encoder { @Override public void encode(Object object, Type bodyType, RequestTemplate template) { if (bodyType == String.class) { - template.body(object.toString()); + template.body(Request.Body.of(object.toString())); } else if (bodyType == byte[].class) { - template.body((byte[]) object, null); + template.body(Request.Body.of((byte[]) object)); } else if (object != null) { throw new EncodeException( format("%s is not a type supported by this encoder.", object.getClass())); diff --git a/core/src/main/java/feign/utils/ThrowingConsumer.java b/core/src/main/java/feign/utils/ThrowingConsumer.java new file mode 100644 index 0000000000..924f8bc705 --- /dev/null +++ b/core/src/main/java/feign/utils/ThrowingConsumer.java @@ -0,0 +1,34 @@ +/* + * 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.utils; + +/** + * A functional interface similar to {@link java.util.function.Consumer} that allows for checked + * exceptions. + * + * @param the type of the input to the operation + * @param the type of exception that may be thrown + */ +@FunctionalInterface +public interface ThrowingConsumer { + /** + * Performs this operation on the given argument. + * + * @param t the input argument + * @throws E if an exception occurs during the operation + */ + void accept(T t) throws E; +} diff --git a/core/src/test/java/feign/AlwaysEncodeBodyContractTest.java b/core/src/test/java/feign/AlwaysEncodeBodyContractTest.java index 7a626d6ef0..a3100d882d 100644 --- a/core/src/test/java/feign/AlwaysEncodeBodyContractTest.java +++ b/core/src/test/java/feign/AlwaysEncodeBodyContractTest.java @@ -17,6 +17,7 @@ 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; @@ -65,7 +66,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(body); + template.body(Request.Body.of(body)); } } @@ -73,14 +74,22 @@ private static class BodyParameterSampleEncoder implements Encoder { @Override public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { - template.body(String.valueOf(object)); + template.body(Request.Body.of(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()).build(); + 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); } } diff --git a/core/src/test/java/feign/AsyncFeignTest.java b/core/src/test/java/feign/AsyncFeignTest.java index efd965632e..08c70f19c4 100644 --- a/core/src/test/java/feign/AsyncFeignTest.java +++ b/core/src/test/java/feign/AsyncFeignTest.java @@ -18,7 +18,6 @@ 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; @@ -584,7 +583,7 @@ void throwsFeignExceptionIncludingBody() throws Throwable { } catch (FeignException e) { assertThat(e.getMessage()) .contains("timeout reading POST http://localhost:" + server.getPort() + "/"); - assertThat(e.contentUTF8()).isEqualTo("Request body"); + assertThat(e.contentUTF8()).isEmpty(); return; } fail(""); @@ -699,7 +698,7 @@ void whenReturnTypeIsResponseNoErrorHandling() throws Throwable { .status(302) .reason("Found") .headers(headers) - .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, null)) .body(new byte[0]) .build(); @@ -970,7 +969,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, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(new HashMap<>()) .build(); } @@ -1227,9 +1226,9 @@ static final class TestInterfaceAsyncBuilder { .encoder( (object, _, template) -> { if (object instanceof Map) { - template.body(new Gson().toJson(object)); + template.body(Request.Body.of(new Gson().toJson(object))); } else { - template.body(object.toString()); + template.body(Request.Body.of(object.toString())); } }); diff --git a/core/src/test/java/feign/ClientTest.java b/core/src/test/java/feign/ClientTest.java index cdfe54d292..c094cadabf 100644 --- a/core/src/test/java/feign/ClientTest.java +++ b/core/src/test/java/feign/ClientTest.java @@ -61,13 +61,12 @@ 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, body, requestTemplate); + Request.HttpMethod.GET, "http://example.com", headers, null, requestTemplate); DefaultClient defaultClient = new DefaultClient(null, null); HttpURLConnection urlConnection = defaultClient.convertAndSend(request, options); Map> requestProperties = urlConnection.getRequestProperties(); @@ -82,13 +81,12 @@ 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, body, requestTemplate); + Request.HttpMethod.GET, "http://example.com", headers, null, 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 6b26d1ed0e..0ecf46f4ae 100644 --- a/core/src/test/java/feign/DefaultContractTest.java +++ b/core/src/test/java/feign/DefaultContractTest.java @@ -17,7 +17,6 @@ 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; @@ -145,7 +144,15 @@ void headersOnMethodAddsContentTypeHeader() throws Exception { assertThat(md.template()) .hasHeaders( entry("Content-Type", asList("application/xml")), - entry("Content-Length", asList(String.valueOf(md.template().body().length)))); + entry( + "Content-Length", + asList( + md.template() + .requestBody() + .map(Request.Body::contentLength) + .filter(contentLength -> contentLength >= 0) + .map(String::valueOf) + .orElse("0")))); } @Test @@ -155,7 +162,15 @@ void headersOnTypeAddsContentTypeHeader() throws Exception { assertThat(md.template()) .hasHeaders( entry("Content-Type", asList("application/xml")), - entry("Content-Length", asList(String.valueOf(md.template().body().length)))); + entry( + "Content-Length", + asList( + md.template() + .requestBody() + .map(Request.Body::contentLength) + .filter(contentLength -> contentLength >= 0) + .map(String::valueOf) + .orElse("0")))); } @Test @@ -165,7 +180,15 @@ void headersContainsWhitespaces() throws Exception { assertThat(md.template()) .hasHeaders( entry("Content-Type", Collections.singletonList("application/xml")), - entry("Content-Length", asList(String.valueOf(md.template().body().length)))); + entry( + "Content-Length", + asList( + md.template() + .requestBody() + .map(Request.Body::contentLength) + .filter(contentLength -> contentLength >= 0) + .map(String::valueOf) + .orElse("0")))); } @Test diff --git a/core/src/test/java/feign/FeignBuilderTest.java b/core/src/test/java/feign/FeignBuilderTest.java index 2db2fd9df5..d9ff04604d 100644 --- a/core/src/test/java/feign/FeignBuilderTest.java +++ b/core/src/test/java/feign/FeignBuilderTest.java @@ -16,7 +16,6 @@ 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; @@ -210,7 +209,7 @@ void overrideEncoder() throws Exception { server.enqueue(new MockResponse().setBody("response data")); String url = "http://localhost:" + server.getPort(); - Encoder encoder = (object, _, template) -> template.body(object.toString()); + Encoder encoder = (object, _, template) -> template.body(Request.Body.of(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 f86d35fe19..660af75fcf 100644 --- a/core/src/test/java/feign/FeignExceptionTest.java +++ b/core/src/test/java/feign/FeignExceptionTest.java @@ -39,8 +39,7 @@ void canCreateWithRequestAndResponse() { Request.HttpMethod.GET, "/home", Collections.emptyMap(), - "data".getBytes(StandardCharsets.UTF_8), - StandardCharsets.UTF_8, + Request.Body.of("data".getBytes(StandardCharsets.UTF_8)), null); Response response = @@ -52,7 +51,7 @@ void canCreateWithRequestAndResponse() { FeignException exception = FeignException.errorReading(request, response, new IOException("socket closed")); - assertThat(exception.responseBody()).isNotEmpty(); + assertThat(exception.responseBody()).isEmpty(); assertThat(exception.hasRequest()).isTrue(); assertThat(exception.request()).isNotNull(); } @@ -64,14 +63,12 @@ void canCreateWithRequestOnly() { Request.HttpMethod.GET, "/home", Collections.emptyMap(), - "data".getBytes(StandardCharsets.UTF_8), - StandardCharsets.UTF_8, + Request.Body.of("data".getBytes(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(); } @@ -90,8 +87,7 @@ void createFeignExceptionWithCorrectCharsetResponse() { Request.HttpMethod.GET, "/home", Collections.emptyMap(), - "data".getBytes(StandardCharsets.UTF_16BE), - StandardCharsets.UTF_16BE, + Request.Body.of("data".getBytes(StandardCharsets.UTF_16BE)), null); Response response = @@ -127,8 +123,7 @@ void createFeignExceptionWithCorrectCharsetResponseButDifferentContentTypeFormat Request.HttpMethod.GET, "/home", Collections.emptyMap(), - "data".getBytes(StandardCharsets.UTF_16BE), - StandardCharsets.UTF_16BE, + Request.Body.of("data".getBytes(StandardCharsets.UTF_16BE)), null); Response response = @@ -158,8 +153,7 @@ void createFeignExceptionWithErrorCharsetResponse() { Request.HttpMethod.GET, "/home", Collections.emptyMap(), - "data".getBytes(StandardCharsets.UTF_16BE), - StandardCharsets.UTF_16BE, + Request.Body.of("data".getBytes(StandardCharsets.UTF_16BE)), null); Response response = @@ -182,8 +176,7 @@ void canGetResponseHeadersFromException() { Request.HttpMethod.GET, "/home", Collections.emptyMap(), - "data".getBytes(StandardCharsets.UTF_8), - StandardCharsets.UTF_8, + Request.Body.of("data".getBytes(StandardCharsets.UTF_8)), null); Map> responseHeaders = new HashMap<>(); @@ -241,8 +234,7 @@ void lengthOfBodyExceptionTest() { Request.HttpMethod.GET, "/home", Collections.emptyMap(), - "data".getBytes(StandardCharsets.UTF_8), - StandardCharsets.UTF_8, + Request.Body.of("data".getBytes(StandardCharsets.UTF_8)), null); Response response = @@ -273,7 +265,12 @@ void nullRequestShouldThrowNPEwThrowableAndBytes() { NullPointerException.class, () -> new Derived( - 404, "message", null, new Throwable(), new byte[1], Collections.emptyMap())); + 404, + "message", + null, + new Throwable(), + "content".getBytes(StandardCharsets.UTF_8), + Collections.emptyMap())); } @Test @@ -285,7 +282,13 @@ void nullRequestShouldThrowNPE() { void nullRequestShouldThrowNPEwBytes() { assertThrows( NullPointerException.class, - () -> new Derived(404, "message", null, new byte[1], Collections.emptyMap())); + () -> + new Derived( + 404, + "message", + null, + "content".getBytes(StandardCharsets.UTF_8), + 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 748bfe1ad5..e6c7f235e0 100755 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -19,7 +19,6 @@ 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 +605,7 @@ void throwsFeignExceptionIncludingBody() { } catch (FeignException e) { assertThat(e.getMessage()) .isEqualTo("timeout reading POST http://localhost:" + server.getPort() + "/"); - assertThat(e.contentUTF8()).isEqualTo("Request body"); + assertThat(e.contentUTF8()).isEmpty(); } } @@ -717,7 +716,7 @@ void whenReturnTypeIsResponseNoErrorHandling() { .status(302) .reason("Found") .headers(headers) - .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, null)) .body(new byte[0]) .build(); @@ -907,7 +906,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, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(new HashMap<>()) .build(); } @@ -1431,9 +1430,9 @@ static final class TestInterfaceBuilder { .encoder( (object, _, template) -> { if (object instanceof Map) { - template.body(new Gson().toJson(object)); + template.body(Request.Body.of(new Gson().toJson(object))); } else { - template.body(object.toString()); + template.body(Request.Body.of(object.toString())); } }); diff --git a/core/src/test/java/feign/FeignUnderAsyncTest.java b/core/src/test/java/feign/FeignUnderAsyncTest.java index f18fd5d747..0440fad8a6 100644 --- a/core/src/test/java/feign/FeignUnderAsyncTest.java +++ b/core/src/test/java/feign/FeignUnderAsyncTest.java @@ -17,7 +17,6 @@ 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; @@ -493,7 +492,7 @@ void throwsFeignExceptionIncludingBody() { } catch (FeignException e) { assertThat(e.getMessage()) .isEqualTo("timeout reading POST http://localhost:" + server.getPort() + "/"); - assertThat(e.contentUTF8()).isEqualTo("Request body"); + assertThat(e.contentUTF8()).isEmpty(); } } @@ -527,7 +526,7 @@ void whenReturnTypeIsResponseNoErrorHandling() { .status(302) .reason("Found") .headers(headers) - .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, null)) .body(new byte[0]) .build(); @@ -722,7 +721,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, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(new HashMap<>()) .build(); } @@ -964,9 +963,9 @@ static final class TestInterfaceBuilder { .encoder( (object, _, template) -> { if (object instanceof Map) { - template.body(new Gson().toJson(object)); + template.body(Request.Body.of(new Gson().toJson(object))); } else { - template.body(object.toString()); + template.body(Request.Body.of(object.toString())); } }); diff --git a/core/src/test/java/feign/LoggerMethodsTest.java b/core/src/test/java/feign/LoggerMethodsTest.java index bee76be63b..36c8c6a913 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, UTF_8, null); + Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null); Response response = Response.builder() .status(200) diff --git a/core/src/test/java/feign/RequestTemplateTest.java b/core/src/test/java/feign/RequestTemplateTest.java index 973ee2104c..183aaca0d4 100644 --- a/core/src/test/java/feign/RequestTemplateTest.java +++ b/core/src/test/java/feign/RequestTemplateTest.java @@ -17,7 +17,6 @@ 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; @@ -92,11 +91,9 @@ void resolveTemplateWithParameterizedPathSkipsEncodingSlash() { @Test void resolveTemplateWithBinaryBody() { + byte[] body = new byte[] {7, 3, -3, -7}; RequestTemplate template = - new RequestTemplate() - .method(HttpMethod.GET) - .uri("{zoneId}") - .body(new byte[] {7, 3, -3, -7}, null); + new RequestTemplate().method(HttpMethod.GET).uri("{zoneId}").body(Request.Body.of(body)); template = template.resolve(mapOf("zoneId", "/hostedzone/Z1PA6795UKMFR9")); assertThat(template).hasUrl("/hostedzone/Z1PA6795UKMFR9"); @@ -345,7 +342,11 @@ void resolveTemplateWithBodyTemplateSetsBodyAndContentLength() { .hasHeaders( entry( "Content-Length", - Collections.singletonList(String.valueOf(template.body().length)))); + Collections.singletonList( + template + .requestBody() + .map(body -> String.valueOf(body.contentLength())) + .orElse("0")))); } @Test diff --git a/core/src/test/java/feign/ResponseTest.java b/core/src/test/java/feign/ResponseTest.java index 9af1ceb887..5a7020f0ff 100644 --- a/core/src/test/java/feign/ResponseTest.java +++ b/core/src/test/java/feign/ResponseTest.java @@ -36,8 +36,7 @@ void reasonPhraseIsOptional() { Response.builder() .status(200) .headers(Collections.>emptyMap()) - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .body(new byte[0]) .build(); @@ -54,8 +53,7 @@ void canAccessHeadersCaseInsensitively() { Response.builder() .status(200) .headers(headersMap) - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .body(new byte[0]) .build(); assertThat(response.headers()) @@ -80,8 +78,7 @@ void charsetSupportsMediaTypesWithQuotedCharset() { Response.builder() .status(200) .headers(headersMap) - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .body(new byte[0]) .build(); assertThat(response.charset()).isEqualTo(Util.UTF_8); @@ -97,8 +94,7 @@ void headerValuesWithSameNameOnlyVaryingInCaseAreMerged() { Response.builder() .status(200) .headers(headersMap) - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .body(new byte[0]) .build(); @@ -115,8 +111,7 @@ void headersAreOptional() { Response response = Response.builder() .status(200) - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .body(new byte[0]) .build(); assertThat(response.headers()).isNotNull().isEmpty(); @@ -127,8 +122,7 @@ void support1xxStatusCodes() { Response response = Response.builder() .status(103) - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .body((Response.Body) null) .build(); @@ -145,7 +139,7 @@ void statusCodesOfAnyValueAreAllowed() { .status(statusCode) .request( Request.create( - HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .body((Response.Body) null) .build(); @@ -158,8 +152,7 @@ void protocolVersionDefaultsToHttp1_1() { Response response = Response.builder() .status(200) - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .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 0bd9a73127..90fc028eb9 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, Util.UTF_8); + Request.create(Request.HttpMethod.GET, "/", Collections.emptyMap(), null, null); 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, Util.UTF_8); + Request.create(Request.HttpMethod.GET, "/", Collections.emptyMap(), null, null); 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, Util.UTF_8); + Request.create(Request.HttpMethod.GET, "/", Collections.emptyMap(), null, null); // when RetryableException retryableException = diff --git a/core/src/test/java/feign/RetryerTest.java b/core/src/test/java/feign/RetryerTest.java index 74ed8e832b..d2656b8686 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, Util.UTF_8); + Request.create(Request.HttpMethod.GET, "/", Collections.emptyMap(), null, null); @Test void only5TriesAllowedAndExponentialBackoff() { diff --git a/core/src/test/java/feign/TargetTest.java b/core/src/test/java/feign/TargetTest.java index 024a9514f5..73e9fb5e92 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(), - urlEncoded.charset()); + urlEncoded.body().orElse(null), + null); } }; diff --git a/core/src/test/java/feign/assertj/RequestTemplateAssert.java b/core/src/test/java/feign/assertj/RequestTemplateAssert.java index 0a571ebb36..d3ccb7a7b9 100644 --- a/core/src/test/java/feign/assertj/RequestTemplateAssert.java +++ b/core/src/test/java/feign/assertj/RequestTemplateAssert.java @@ -15,9 +15,11 @@ */ package feign.assertj; -import static feign.Util.UTF_8; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +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; @@ -58,7 +60,8 @@ public RequestTemplateAssert hasBody(String utf8Expected) { if (actual.bodyTemplate() != null) { failWithMessage("\nExpecting bodyTemplate to be null, but was:<%s>", actual.bodyTemplate()); } - objects.assertEqual(info, new String(actual.body(), UTF_8), utf8Expected); + objects.assertEqual( + info, actual.requestBody().map(this::bodyAsUtf8String).orElse(null), utf8Expected); return this; } @@ -67,13 +70,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.body(), expected); + arrays.assertContains(info, actual.requestBody().map(this::bodyAsBytes).orElse(null), expected); return this; } public RequestTemplateAssert hasBodyTemplate(String expected) { isNotNull(); - if (actual.body() != null) { + if (actual.requestBody().isPresent()) { failWithMessage("\nExpecting body to be null, but was:<%s>", actual.bodyTemplate()); } objects.assertEqual(info, actual.bodyTemplate(), expected); @@ -99,17 +102,24 @@ public RequestTemplateAssert hasNoHeader(final String encoded) { public RequestTemplateAssert noRequestBody() { isNotNull(); - if (actual.body() != null) { + if (actual.requestBody().isPresent()) { if (actual.bodyTemplate() != null) { failWithMessage( "\nExpecting requestBody.bodyTemplate to be null, but was:<%s>", actual.bodyTemplate()); } - if (actual.body() != null) { + if (actual.requestBody().isPresent()) { failWithMessage( - "\nExpecting requestBody.data to be null, but was:<%s>", - new String(actual.body(), actual.requestCharset())); + "\nExpecting requestBody.data to be null, but was:<%s>", actual.requestBody().get()); } } 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 23ba1d4d06..f1856c2946 100644 --- a/core/src/test/java/feign/codec/DefaultDecoderTest.java +++ b/core/src/test/java/feign/codec/DefaultDecoderTest.java @@ -22,7 +22,6 @@ 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; @@ -74,7 +73,7 @@ private Response knownResponse() { .status(200) .reason("OK") .headers(headers) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .body(inputStream, content.length()) .build(); } @@ -84,7 +83,7 @@ private Response nullBodyResponse() { .status(200) .reason("OK") .headers(Collections.>emptyMap()) - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .build(); } } diff --git a/core/src/test/java/feign/codec/DefaultEncoderTest.java b/core/src/test/java/feign/codec/DefaultEncoderTest.java index 9aecbb9059..94f5183229 100644 --- a/core/src/test/java/feign/codec/DefaultEncoderTest.java +++ b/core/src/test/java/feign/codec/DefaultEncoderTest.java @@ -15,13 +15,15 @@ */ 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.Arrays; +import java.util.Optional; import org.junit.jupiter.api.Test; class DefaultEncoderTest { @@ -33,7 +35,11 @@ void encodesStrings() throws Exception { String content = "This is my content"; RequestTemplate template = new RequestTemplate(); encoder.encode(content, String.class, template); - assertThat(new String(template.body(), UTF_8)).isEqualTo(content); + Optional optionalBody = template.requestBody(); + assertThat(optionalBody).isPresent(); + String body = + assertDoesNotThrow(() -> optionalBody.get().writeToString(StandardCharsets.UTF_8)); + assertThat(body).contains(content); } @Test @@ -41,7 +47,10 @@ void encodesByteArray() throws Exception { byte[] content = {12, 34, 56}; RequestTemplate template = new RequestTemplate(); encoder.encode(content, byte[].class, template); - assertThat(Arrays.equals(content, template.body())).isTrue(); + Optional optionalBody = template.requestBody(); + assertThat(optionalBody).isPresent(); + byte[] body = assertDoesNotThrow(() -> optionalBody.get().writeToByteArray()); + assertThat(body).isEqualTo(content); } @Test diff --git a/core/src/test/java/feign/codec/DefaultErrorDecoderHttpErrorTest.java b/core/src/test/java/feign/codec/DefaultErrorDecoderHttpErrorTest.java index c9dae156bd..0d546b1cd7 100644 --- a/core/src/test/java/feign/codec/DefaultErrorDecoderHttpErrorTest.java +++ b/core/src/test/java/feign/codec/DefaultErrorDecoderHttpErrorTest.java @@ -141,11 +141,7 @@ void exceptionIsHttpSpecific(int httpStatus, Class expectedExceptionClass, Strin .reason("anything") .request( Request.create( - HttpMethod.GET, - "http://example.com/api", - Collections.emptyMap(), - null, - Util.UTF_8)) + HttpMethod.GET, "http://example.com/api", Collections.emptyMap(), null, null)) .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 1967e77632..6088ea54b1 100644 --- a/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java +++ b/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java @@ -26,6 +26,7 @@ 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; @@ -47,8 +48,7 @@ void throwsFeignException() throws Throwable { Response.builder() .status(500) .reason("Internal server error") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(headers) .build(); @@ -63,8 +63,7 @@ void throwsFeignExceptionIncludingBody() throws Throwable { Response.builder() .status(500) .reason("Internal server error") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(headers) .body("hello world", UTF_8) .build(); @@ -86,8 +85,7 @@ void throwsFeignExceptionIncludingLongBody() throws Throwable { Response.builder() .status(500) .reason("Internal server error") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(headers) .body(actualBody, UTF_8) .build(); @@ -119,8 +117,7 @@ void feignExceptionIncludesStatus() { Response.builder() .status(400) .reason("Bad request") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(headers) .build(); @@ -138,8 +135,7 @@ void retryAfterHeaderThrowsRetryableException() throws Throwable { Response.builder() .status(503) .reason("Service Unavailable") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(headers) .build(); @@ -194,8 +190,7 @@ private Response bigBodyResponse() { Request.HttpMethod.GET, "/home", Collections.emptyMap(), - "data".getBytes(Util.UTF_8), - Util.UTF_8, + Request.Body.of("data".getBytes(StandardCharsets.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 d514dcc6b7..86a6d8509e 100644 --- a/core/src/test/java/feign/stream/StreamDecoderTest.java +++ b/core/src/test/java/feign/stream/StreamDecoderTest.java @@ -24,7 +24,6 @@ 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; @@ -138,8 +137,7 @@ void shouldCloseIteratorWhenStreamClosed() throws IOException { .status(200) .reason("OK") .headers(Collections.emptyMap()) - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .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 2a2c644c5f..4263d14d51 100644 --- a/dropwizard-metrics4/src/main/java/feign/metrics4/MeteredEncoder.java +++ b/dropwizard-metrics4/src/main/java/feign/metrics4/MeteredEncoder.java @@ -50,13 +50,15 @@ public void encode(Object object, Type bodyType, RequestTemplate template) encoder.encode(object, bodyType, template); } - if (template.body() != null) { - metricRegistry - .histogram( - metricName.metricName( - template.methodMetadata(), template.feignTarget(), "request_size"), - metricSuppliers.histograms()) - .update(template.body().length); - } + template + .requestBody() + .ifPresent( + body -> + metricRegistry + .histogram( + metricName.metricName( + template.methodMetadata(), template.feignTarget(), "request_size"), + metricSuppliers.histograms()) + .update(body.contentLength())); } } diff --git a/dropwizard-metrics5/src/main/java/feign/metrics5/MeteredEncoder.java b/dropwizard-metrics5/src/main/java/feign/metrics5/MeteredEncoder.java index cd0ee72cf5..8315a214dd 100644 --- a/dropwizard-metrics5/src/main/java/feign/metrics5/MeteredEncoder.java +++ b/dropwizard-metrics5/src/main/java/feign/metrics5/MeteredEncoder.java @@ -50,13 +50,15 @@ public void encode(Object object, Type bodyType, RequestTemplate template) encoder.encode(object, bodyType, template); } - if (template.body() != null) { - metricRegistry - .histogram( - metricName.metricName( - template.methodMetadata(), template.feignTarget(), "request_size"), - metricSuppliers.histograms()) - .update(template.body().length); - } + template + .requestBody() + .ifPresent( + body -> + metricRegistry + .histogram( + metricName.metricName( + template.methodMetadata(), template.feignTarget(), "request_size"), + metricSuppliers.histograms()) + .update(body.contentLength())); } } diff --git a/fastjson2/src/main/java/feign/fastjson2/Fastjson2Encoder.java b/fastjson2/src/main/java/feign/fastjson2/Fastjson2Encoder.java index 06a98e8dd2..7e79af331a 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(JSON.toJSONBytes(object, features), Util.UTF_8); + template.body(Request.Body.of(JSON.toJSONBytes(object, features))); } } diff --git a/fastjson2/src/test/java/feign/fastjson2/FastJsonCodecTest.java b/fastjson2/src/test/java/feign/fastjson2/FastJsonCodecTest.java index 606ccf2511..06c00502d5 100644 --- a/fastjson2/src/test/java/feign/fastjson2/FastJsonCodecTest.java +++ b/fastjson2/src/test/java/feign/fastjson2/FastJsonCodecTest.java @@ -22,7 +22,6 @@ 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; @@ -107,8 +106,7 @@ void decodes() throws Exception { .status(200) .reason("OK") .request( - Request.create( - Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(zonesJson, UTF_8) .build(); @@ -124,8 +122,7 @@ void nullBodyDecodesToNull() throws Exception { .status(204) .reason("OK") .request( - Request.create( - Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .build(); assertThat(new Fastjson2Decoder().decode(response, String.class)).isNull(); @@ -138,8 +135,7 @@ void emptyBodyDecodesToNull() throws Exception { .status(204) .reason("OK") .request( - Request.create( - Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(new byte[0]) .build(); @@ -158,8 +154,7 @@ void decoderCharset() throws IOException { .status(200) .reason("OK") .request( - Request.create( - Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(headers) .body( new String( @@ -205,8 +200,7 @@ void notFoundDecodesToEmpty() throws Exception { .status(404) .reason("NOT FOUND") .request( - Request.create( - Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .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 af31d36eb6..d8c6e9228f 100644 --- a/form/src/main/java/feign/form/MultipartFormContentProcessor.java +++ b/form/src/main/java/feign/form/MultipartFormContentProcessor.java @@ -18,6 +18,7 @@ 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; @@ -103,7 +104,7 @@ public void process(RequestTemplate template, Charset charset, MapemptyList()); // reset header template.header(CONTENT_TYPE_HEADER, contentTypeValue); - template.body(bytes, charset); + template.body(Request.Body.of(bytes)); } @Override diff --git a/form/src/main/java/feign/form/multipart/DelegateWriter.java b/form/src/main/java/feign/form/multipart/DelegateWriter.java index 7161c21a46..3295741e4e 100644 --- a/form/src/main/java/feign/form/multipart/DelegateWriter.java +++ b/form/src/main/java/feign/form/multipart/DelegateWriter.java @@ -17,9 +17,12 @@ 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; @@ -46,8 +49,15 @@ 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); - val bytes = fake.body(); - val string = new String(bytes, output.getCharset()).replaceAll("\n", ""); - parameterWriter.write(output, key, string); + 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); + } } } diff --git a/googlehttpclient/src/main/java/feign/googlehttpclient/GoogleHttpClient.java b/googlehttpclient/src/main/java/feign/googlehttpclient/GoogleHttpClient.java index a57101a89c..65aa8f1180 100644 --- a/googlehttpclient/src/main/java/feign/googlehttpclient/GoogleHttpClient.java +++ b/googlehttpclient/src/main/java/feign/googlehttpclient/GoogleHttpClient.java @@ -15,7 +15,6 @@ */ 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; @@ -28,6 +27,7 @@ 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,18 +66,7 @@ public final Response execute(final Request inputRequest, final Request.Options private final HttpRequest convertRequest( final Request inputRequest, final Request.Options options) throws IOException { - // 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()); - } + final HttpContent content = toHttpContent(inputRequest); // Build the request final HttpRequest request = @@ -108,6 +97,21 @@ 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(); @@ -131,4 +135,34 @@ 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 4486a239f4..78d8d25efe 100644 --- a/graphql/src/main/java/feign/graphql/GraphqlDecoder.java +++ b/graphql/src/main/java/feign/graphql/GraphqlDecoder.java @@ -16,6 +16,7 @@ package feign.graphql; import feign.Experimental; +import feign.Request; import feign.Response; import feign.Util; import feign.codec.Decoder; @@ -111,14 +112,14 @@ private String resolveOperationField(Map root, Response response } } - if (response.request() != null && response.request().body() != null) { + if (response.request() != null && response.request().body().isPresent()) { try { var fakeResponse = Response.builder() .status(200) .headers(Collections.emptyMap()) .request(response.request()) - .body(response.request().body()) + .body(bodyAsByteArray(response.request())) .build(); var requestBody = (Map) jsonDecoder.decode(fakeResponse, Map.class); if (requestBody != null) { @@ -135,6 +136,12 @@ 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 252834fc3d..5a318958ca 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.body() != null) { + if (template.requestBody().isPresent()) { return; } diff --git a/graphql/src/test/java/feign/graphql/GraphqlDecoderTest.java b/graphql/src/test/java/feign/graphql/GraphqlDecoderTest.java index b796a68612..13d92f3aac 100644 --- a/graphql/src/test/java/feign/graphql/GraphqlDecoderTest.java +++ b/graphql/src/test/java/feign/graphql/GraphqlDecoderTest.java @@ -332,11 +332,7 @@ private Response buildResponse(String body) { private Request buildRequest() { return Request.create( - HttpMethod.POST, - "http://localhost/graphql", - Collections.emptyMap(), - Request.Body.empty(), - null); + HttpMethod.POST, "http://localhost/graphql", Collections.emptyMap(), null, 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 07a22ed8a9..21444de39a 100644 --- a/graphql/src/test/java/feign/graphql/GraphqlEncoderTest.java +++ b/graphql/src/test/java/feign/graphql/GraphqlEncoderTest.java @@ -16,10 +16,13 @@ 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; @@ -51,13 +54,29 @@ 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(template.body()); + var result = mapper.readTree(requestBodyBytes(template)); assertThat(result.has("query")).isTrue(); assertThat(result.get("query").asText()).contains("createUser"); assertThat(result.has("variables")).isTrue(); @@ -69,7 +88,7 @@ void encodesBodyWithVariables() throws Exception { void delegatesToWrappedEncoderForNonGraphql() { var template = new RequestTemplate(); encoder.encode("plain body", String.class, template); - assertThat(template.body()).isNotNull(); + assertThat(template.requestBody()).isNotEmpty(); } @Test @@ -77,7 +96,7 @@ void interceptorSetsBodyForNoVariableQuery() throws Exception { var template = templateFor(NoVariableApi.class); interceptor.apply(template); - var result = mapper.readTree(template.body()); + var result = mapper.readTree(requestBodyBytes(template)); assertThat(result.get("query").asText()).contains("pending"); assertThat(result.has("variables")).isFalse(); } @@ -85,16 +104,16 @@ void interceptorSetsBodyForNoVariableQuery() throws Exception { @Test void interceptorSkipsWhenBodyAlreadySet() { var template = templateFor(MutationApi.class); - template.body("already set"); + template.body(Request.Body.of("already set")); interceptor.apply(template); - assertThat(new String(template.body())).isEqualTo("already set"); + assertThat(requestBodyString(template)).isEqualTo("already set"); } @Test void interceptorSkipsForNonGraphql() { var template = new RequestTemplate(); - template.body("some body"); + template.body(Request.Body.of("some body")); interceptor.apply(template); - assertThat(new String(template.body())).isEqualTo("some body"); + assertThat(requestBodyString(template)).isEqualTo("some body"); } } diff --git a/gson/src/main/java/feign/gson/GsonEncoder.java b/gson/src/main/java/feign/gson/GsonEncoder.java index c4484bc6eb..c0c3d0eb2f 100644 --- a/gson/src/main/java/feign/gson/GsonEncoder.java +++ b/gson/src/main/java/feign/gson/GsonEncoder.java @@ -17,6 +17,7 @@ import com.google.gson.Gson; import com.google.gson.TypeAdapter; +import feign.Request; import feign.RequestTemplate; import feign.codec.Encoder; import feign.codec.JsonEncoder; @@ -41,6 +42,6 @@ public GsonEncoder(Gson gson) { @Override public void encode(Object object, Type bodyType, RequestTemplate template) { - template.body(gson.toJson(object, bodyType)); + template.body(Request.Body.of(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 1ec47b8918..f1a99670bb 100644 --- a/gson/src/test/java/feign/gson/GsonCodecTest.java +++ b/gson/src/test/java/feign/gson/GsonCodecTest.java @@ -27,7 +27,6 @@ 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; @@ -66,8 +65,7 @@ void decodesMapObjectNumericalValuesAsInteger() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body("{\"foo\": 1}", UTF_8) .build(); @@ -131,8 +129,7 @@ void decodes() throws Exception { .status(200) .reason("OK") .headers(Collections.emptyMap()) - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .body(zonesJson, UTF_8) .build(); assertThat(new GsonDecoder().decode(response, new TypeToken>() {}.getType())) @@ -146,8 +143,7 @@ void nullBodyDecodesToNull() throws Exception { .status(204) .reason("OK") .headers(Collections.emptyMap()) - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .build(); assertThat(new GsonDecoder().decode(response, String.class)).isNull(); } @@ -159,8 +155,7 @@ void emptyBodyDecodesToNull() throws Exception { .status(204) .reason("OK") .headers(Collections.emptyMap()) - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .body(new byte[0]) .build(); assertThat(new GsonDecoder().decode(response, String.class)).isNull(); @@ -216,8 +211,7 @@ void customDecoder() throws Exception { .status(200) .reason("OK") .headers(Collections.emptyMap()) - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .body(zonesJson, UTF_8) .build(); assertThat(decoder.decode(response, new TypeToken>() {}.getType())).isEqualTo(zones); @@ -256,8 +250,7 @@ void notFoundDecodesToEmpty() throws Exception { .status(404) .reason("NOT FOUND") .headers(Collections.emptyMap()) - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .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 65edd9e71c..daaac3b77a 100644 --- a/hc5/src/main/java/feign/hc5/ApacheHttp5Client.java +++ b/hc5/src/main/java/feign/hc5/ApacheHttp5Client.java @@ -49,7 +49,6 @@ 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; @@ -157,22 +156,8 @@ ClassicHttpRequest toClassicHttpRequest(Request request, Request.Options options } // request body - // 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 (request.body().isPresent()) { + HttpEntity entity = new FeignBodyEntity(request.body().get(), getContentType(request)); if (isGzip) { entity = new GzipCompressingEntity(entity); } @@ -191,9 +176,6 @@ 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 f51c1c2221..82bc294cae 100644 --- a/hc5/src/main/java/feign/hc5/AsyncApacheHttp5Client.java +++ b/hc5/src/main/java/feign/hc5/AsyncApacheHttp5Client.java @@ -17,24 +17,42 @@ import static feign.Util.enumForName; -import feign.*; +import feign.AsyncClient; +import feign.Request; import feign.Request.Options; -import java.io.ByteArrayOutputStream; +import feign.Response; +import feign.Util; import java.io.IOException; -import java.util.*; +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.Optional; import java.util.concurrent.CompletableFuture; -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 @@ -69,7 +87,15 @@ private static CloseableHttpAsyncClient createStartedClient() { @Override public CompletableFuture execute( Request request, Options options, Optional requestContext) { - final SimpleHttpRequest httpUriRequest = toClassicHttpRequest(request, options); + 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 CompletableFuture result = new CompletableFuture<>(); final FutureCallback callback = @@ -90,12 +116,25 @@ public void cancelled() { result.cancel(false); } }; + final ClassicToAsyncRequestProducer requestProducer = + new ClassicToAsyncRequestProducer( + httpUriRequest, Timeout.of(options.connectTimeout(), options.connectTimeoutUnit())); client.execute( - httpUriRequest, + requestProducer, + SimpleResponseConsumer.create(), configureTimeoutsAndRedirection(options, requestContext.orElseGet(HttpClientContext::new)), callback); + CompletableFuture.runAsync( + () -> { + try { + requestProducer.blockWaiting().execute(); + } catch (IOException | InterruptedException e) { + result.completeExceptionally(e); + } + }); + return result; } @@ -114,9 +153,20 @@ protected HttpClientContext configureTimeoutsAndRedirection( return context; } - SimpleHttpRequest toClassicHttpRequest(Request request, Request.Options options) { - final SimpleHttpRequest httpRequest = - new SimpleHttpRequest(request.httpMethod().name(), request.url()); + 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); + } // request headers boolean hasAcceptHeader = false; @@ -142,34 +192,27 @@ SimpleHttpRequest toClassicHttpRequest(Request request, Request.Options options) "Deflate Content-Encoding is not supported by feign-hc5"); } } - for (final String headerValue : headerEntry.getValue()) { - httpRequest.addHeader(headerName, headerValue); + requestBuilder.addHeader(headerName, headerValue); } } // some servers choke on the default accept string, so we'll set it to anything if (!hasAcceptHeader) { - httpRequest.addHeader(ACCEPT_HEADER_NAME, "*/*"); + requestBuilder.addHeader(ACCEPT_HEADER_NAME, "*/*"); } // request body - // 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 + if (request.body().isPresent()) { + HttpEntity entity = new FeignBodyEntity(request.body().get(), getContentType(request)); + if (isGzip) { + entity = new GzipCompressingEntity(entity); } - } - if (data != null) { - httpRequest.setBody(data, getContentType(request)); + requestBuilder.setEntity(entity); + } else { + requestBuilder.setEntity(new ByteArrayEntity(new byte[0], null)); } - return httpRequest; + return requestBuilder.build(); } private ContentType getContentType(Request request) { @@ -179,9 +222,6 @@ 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 new file mode 100644 index 0000000000..18504848df --- /dev/null +++ b/hc5/src/main/java/feign/hc5/FeignBodyEntity.java @@ -0,0 +1,97 @@ +/* + * 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 6d5902d9c0..cb33e46dae 100644 --- a/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java +++ b/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java @@ -17,7 +17,6 @@ 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; @@ -535,7 +534,7 @@ void throwsFeignExceptionIncludingBody() throws Throwable { } catch (final FeignException e) { assertThat(e.getMessage()) .isEqualTo("timeout reading POST http://localhost:" + server.getPort() + "/"); - assertThat(e.contentUTF8()).isEqualTo("Request body"); + assertThat(e.contentUTF8()).isEmpty(); return; } fail(""); @@ -572,7 +571,7 @@ void whenReturnTypeIsResponseNoErrorHandling() throws Throwable { .status(302) .reason("Found") .headers(headers) - .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, null)) .body(new byte[0]) .build(); @@ -773,7 +772,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, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(new HashMap<>()) .build(); } @@ -1084,9 +1083,9 @@ static final class TestInterfaceAsyncBuilder { .encoder( (object, _, template) -> { if (object instanceof Map) { - template.body(new Gson().toJson(object)); + template.body(Request.Body.of(new Gson().toJson(object))); } else { - template.body(object.toString()); + template.body(Request.Body.of(object.toString())); } }); diff --git a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java index 010c229cb7..8f023c1e5d 100644 --- a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java +++ b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java @@ -24,6 +24,7 @@ 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; @@ -40,12 +41,15 @@ import org.apache.http.StatusLine; import org.apache.http.client.HttpClient; import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.*; +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.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; @@ -134,24 +138,51 @@ HttpUriRequest toHttpUriRequest(Request request, Request.Options options) } // request body - 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()); - } + HttpEntity entity = + request + .body() + .map(body -> toHttpEntity(body, getContentType(request))) + .orElseGet(() -> new ByteArrayEntity(new byte[0])); - requestBuilder.setEntity(entity); - } else { - requestBuilder.setEntity(new ByteArrayEntity(new byte[0])); - } + requestBuilder.setEntity(entity); 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()) @@ -159,9 +190,6 @@ 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; } } @@ -244,4 +272,42 @@ public void close() throws IOException { } }; } + + private static final class FeignBodyEntity extends AbstractHttpEntity { + + private final Request.Body body; + private final long contentLength; + + private FeignBodyEntity(Request.Body body, ContentType contentType, long contentLength) { + this.body = body; + this.contentLength = contentLength; + setContentType(contentType.toString()); + setChunked(contentLength < 0); + } + + @Override + public long getContentLength() { + return 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(); + } + } } 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 3786edb36e..6e4caeaede 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(outputStream.toByteArray(), Charset.defaultCharset()); + template.body(Request.Body.of(outputStream.toByteArray())); } 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 8e0225d192..29670826ea 100644 --- a/jackson-jaxb/src/test/java/feign/jackson/jaxb/JacksonJaxbCodecTest.java +++ b/jackson-jaxb/src/test/java/feign/jackson/jaxb/JacksonJaxbCodecTest.java @@ -23,7 +23,6 @@ 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; @@ -50,8 +49,7 @@ void decodeTest() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body("{\"value\":\"Test\"}", UTF_8) .build(); @@ -67,8 +65,7 @@ void notFoundDecodesToEmpty() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .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 44118eed76..26fe78179c 100644 --- a/jackson-jr/src/main/java/feign/jackson/jr/JacksonJrEncoder.java +++ b/jackson-jr/src/main/java/feign/jackson/jr/JacksonJrEncoder.java @@ -17,6 +17,7 @@ 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; @@ -53,9 +54,9 @@ public JacksonJrEncoder(Iterable iterable) { public void encode(Object object, Type bodyType, RequestTemplate template) { try { if (bodyType == byte[].class) { - template.body(mapper.asBytes(object), null); + template.body(Request.Body.of(mapper.asBytes(object))); } else { - template.body(mapper.asString(object)); + template.body(Request.Body.of(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 c7c70bd67b..c716bc8194 100644 --- a/jackson-jr/src/test/java/feign/jackson/jr/JacksonCodecTest.java +++ b/jackson-jr/src/test/java/feign/jackson/jr/JacksonCodecTest.java @@ -25,7 +25,6 @@ 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; @@ -96,8 +95,7 @@ void decodes() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(zonesJson, UTF_8) .build(); @@ -112,8 +110,7 @@ void nullBodyDecodesToEmpty() throws Exception { Response.builder() .status(204) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .build(); assertThat((byte[]) new JacksonJrDecoder().decode(response, byte[].class)).isEmpty(); @@ -125,8 +122,7 @@ void emptyBodyDecodesToEmpty() throws Exception { Response.builder() .status(204) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(new byte[0]) .build(); @@ -145,8 +141,7 @@ void customDecoder() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(DATES_JSON, UTF_8) .build(); @@ -167,8 +162,7 @@ void customDecoderExpressedAsMapper() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(DATES_JSON, UTF_8) .build(); @@ -201,8 +195,7 @@ void decoderCharset() throws IOException { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(headers) .body( new String( @@ -228,8 +221,7 @@ void decodesToMap() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(json, UTF_8) .build(); @@ -307,8 +299,7 @@ void notFoundDecodesToEmpty() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .build(); assertThat((byte[]) new JacksonJrDecoder().decode(response, byte[].class)).isEmpty(); @@ -348,12 +339,7 @@ static Stream decodeGenericsArguments() { Response.Builder responseBuilder = Response.builder() .request( - Request.create( - HttpMethod.GET, - "/v1/dummy", - Collections.emptyMap(), - Request.Body.empty(), - null)); + Request.create(HttpMethod.GET, "/v1/dummy", Collections.emptyMap(), null, 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 48b169a8d3..280cd3bb58 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(mapper.writerFor(javaType).writeValueAsBytes(object), Util.UTF_8); + template.body(Request.Body.of(mapper.writerFor(javaType).writeValueAsBytes(object))); } 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 7de8ec3ad4..0097e066ee 100644 --- a/jackson/src/test/java/feign/jackson/JacksonCodecTest.java +++ b/jackson/src/test/java/feign/jackson/JacksonCodecTest.java @@ -32,7 +32,6 @@ 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,8 +120,7 @@ void decodes() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(zonesJson, UTF_8) .build(); @@ -136,8 +134,7 @@ void nullBodyDecodesToNull() throws Exception { Response.builder() .status(204) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .build(); assertThat(new JacksonDecoder().decode(response, String.class)).isNull(); @@ -149,8 +146,7 @@ void emptyBodyDecodesToNull() throws Exception { Response.builder() .status(204) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(new byte[0]) .build(); @@ -171,8 +167,7 @@ void customDecoder() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(zonesJson, UTF_8) .build(); @@ -220,8 +215,7 @@ void decoderCharset() throws IOException { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(headers) .body( new String( @@ -250,8 +244,7 @@ void decodesIterator() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(zonesJson, UTF_8) .build(); @@ -277,8 +270,7 @@ void nullBodyDecodesToEmptyIterator() throws Exception { Response.builder() .status(204) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .build(); assertThat((byte[]) JacksonIteratorDecoder.create().decode(response, byte[].class)).isEmpty(); @@ -290,8 +282,7 @@ void emptyBodyDecodesToEmptyIterator() throws Exception { Response.builder() .status(204) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(new byte[0]) .build(); @@ -364,8 +355,7 @@ void notFoundDecodesToEmpty() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .build(); assertThat((byte[]) new JacksonDecoder().decode(response, byte[].class)).isEmpty(); @@ -378,8 +368,7 @@ void notFoundDecodesToEmptyIterator() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .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 48bc1be058..e935b6d0f8 100644 --- a/jackson/src/test/java/feign/jackson/JacksonIteratorTest.java +++ b/jackson/src/test/java/feign/jackson/JacksonIteratorTest.java @@ -25,7 +25,6 @@ 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; @@ -123,8 +122,7 @@ public void close() throws IOException { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(inputStream, jsonBytes.length) .build(); @@ -150,8 +148,7 @@ public void close() throws IOException { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(inputStream, jsonBytes.length) .build(); @@ -181,8 +178,7 @@ JacksonIterator iterator(Class type, String json) throws IOException { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .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 90342c0162..273e6d8f79 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(mapper.writerFor(javaType).writeValueAsBytes(object), Util.UTF_8); + template.body(Request.Body.of(mapper.writerFor(javaType).writeValueAsBytes(object))); } 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 d91683033c..16236b2c6d 100644 --- a/jackson3/src/test/java/feign/jackson3/Jackson3CodecTest.java +++ b/jackson3/src/test/java/feign/jackson3/Jackson3CodecTest.java @@ -23,7 +23,6 @@ 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; @@ -122,8 +121,7 @@ void decodes() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(zonesJson, UTF_8) .build(); @@ -137,8 +135,7 @@ void nullBodyDecodesToNull() throws Exception { Response.builder() .status(204) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .build(); assertThat(new Jackson3Decoder().decode(response, String.class)).isNull(); @@ -150,8 +147,7 @@ void emptyBodyDecodesToNull() throws Exception { Response.builder() .status(204) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(new byte[0]) .build(); @@ -172,8 +168,7 @@ void customDecoder() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(zonesJson, UTF_8) .build(); @@ -221,8 +216,7 @@ void decoderCharset() throws IOException { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(headers) .body( new String( @@ -251,8 +245,7 @@ void decodesIterator() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(zonesJson, UTF_8) .build(); @@ -278,8 +271,7 @@ void nullBodyDecodesToEmptyIterator() throws Exception { Response.builder() .status(204) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .build(); assertThat((byte[]) Jackson3IteratorDecoder.create().decode(response, byte[].class)).isEmpty(); @@ -291,8 +283,7 @@ void emptyBodyDecodesToEmptyIterator() throws Exception { Response.builder() .status(204) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(new byte[0]) .build(); @@ -365,8 +356,7 @@ void notFoundDecodesToEmpty() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .build(); assertThat((byte[]) new Jackson3Decoder().decode(response, byte[].class)).isEmpty(); @@ -379,8 +369,7 @@ void notFoundDecodesToEmptyIterator() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .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 40d4548a22..c169149816 100644 --- a/java11/src/main/java/feign/http2client/Http2Client.java +++ b/java11/src/main/java/feign/http2client/Http2Client.java @@ -26,6 +26,9 @@ 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; @@ -215,13 +218,8 @@ 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; - final byte[] data = request.body(); - if (data == null) { - body = BodyPublishers.noBody(); - } else { - body = BodyPublishers.ofByteArray(data); - } + final BodyPublisher body = + request.body().map(this::createBodyPublisher).orElseGet(BodyPublishers::noBody); final Builder requestBuilder = HttpRequest.newBuilder() @@ -237,6 +235,30 @@ 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( + () -> { + PipedInputStream inputStream = new PipedInputStream(); + try { + PipedOutputStream outputStream = new PipedOutputStream(inputStream); + CompletableFuture.runAsync( + () -> { + try (outputStream) { + body.writeTo(outputStream); + } catch (IOException ignored) { + } + }); + 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. * diff --git a/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java b/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java index 0d3cd6570b..d205f51470 100644 --- a/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java +++ b/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java @@ -16,7 +16,6 @@ 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; @@ -529,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()).isEqualTo("Request body"); + assertThat(e.contentUTF8()).isEmpty(); return; } fail(""); @@ -566,7 +565,7 @@ void whenReturnTypeIsResponseNoErrorHandling() throws Throwable { .status(302) .reason("Found") .headers(headers) - .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, null)) .body(new byte[0]) .build(); @@ -770,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, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(new HashMap<>()) .build(); } @@ -1028,9 +1027,9 @@ static final class TestInterfaceAsyncBuilder { .encoder( (object, _, template) -> { if (object instanceof Map) { - template.body(new Gson().toJson(object)); + template.body(Request.Body.of(new Gson().toJson(object))); } else { - template.body(object.toString()); + template.body(Request.Body.of(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 4ea3b7e998..a25845800b 100644 --- a/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java +++ b/jaxb-jakarta/src/main/java/feign/jaxb/JAXBEncoder.java @@ -15,6 +15,7 @@ */ package feign.jaxb; +import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; @@ -60,7 +61,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(stringWriter.toString()); + template.body(Request.Body.of(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 d79d3c4725..a4f62453de 100644 --- a/jaxb-jakarta/src/test/java/feign/jaxb/JAXBCodecTest.java +++ b/jaxb-jakarta/src/test/java/feign/jaxb/JAXBCodecTest.java @@ -17,14 +17,13 @@ import static feign.Util.UTF_8; import static feign.assertj.FeignAssertions.assertThat; -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.Request.HttpMethod; import feign.RequestTemplate; import feign.Response; -import feign.Util; import feign.codec.DecodeException; import feign.codec.EncodeException; import feign.codec.Encoder; @@ -199,8 +198,7 @@ void decodesXml() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(mockXml, UTF_8) .build(); @@ -223,8 +221,7 @@ class ParameterizedHolder { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.>emptyMap()) .body("", UTF_8) .build(); @@ -272,10 +269,9 @@ void decodeAnnotatedParameterizedTypes() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.>emptyMap()) - .body(template.body()) + .body(template.requestBody().map(this::bodyAsBytes).orElse(null)) .build(); new JAXBDecoder(new JAXBContextFactory.Builder().build()).decode(response, Box.class); @@ -288,8 +284,7 @@ void notFoundDecodesToEmpty() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.>emptyMap()) .build(); assertThat( @@ -312,8 +307,7 @@ void decodeThrowsExceptionWhenUnmarshallingFailsWithSetSchema() throws Exception Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(mockXml, UTF_8) .build(); @@ -341,8 +335,7 @@ void decodesIgnoringErrorsWithEventHandler() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(mockXml, UTF_8) .build(); @@ -393,6 +386,10 @@ 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 d355afb691..b1f98bd8a2 100644 --- a/jaxb-jakarta/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java +++ b/jaxb-jakarta/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java @@ -16,10 +16,12 @@ 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; @@ -72,8 +74,7 @@ private static String canonicalString(RequestTemplate input, String host) { canonicalRequest.append("host").append('\n'); // HexEncode(Hash(Payload)) - byte[] data = input.body(); - String bodyText = (data != null) ? new String(data, input.requestCharset()) : null; + String bodyText = input.requestBody().map(AWSSignatureVersion4::bodyAsUtf8String).orElse(null); if (bodyText != null) { canonicalRequest.append(hex(sha256(bodyText))); } else { @@ -82,6 +83,10 @@ 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' + @@ -116,7 +121,7 @@ public Request apply(RequestTemplate input) { if (!input.headers().isEmpty()) { throw new UnsupportedOperationException("headers not supported"); } - if (input.body() != null) { + if (input.requestBody().isPresent()) { 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 aae439cae6..4c05d82e71 100644 --- a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java +++ b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java @@ -15,6 +15,7 @@ */ package feign.jaxb; +import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; @@ -60,7 +61,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(stringWriter.toString()); + template.body(Request.Body.of(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 464d857b8c..139615175f 100644 --- a/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java +++ b/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java @@ -17,14 +17,13 @@ import static feign.Util.UTF_8; import static feign.assertj.FeignAssertions.assertThat; -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.Request.HttpMethod; import feign.RequestTemplate; import feign.Response; -import feign.Util; import feign.codec.DecodeException; import feign.codec.EncodeException; import feign.codec.Encoder; @@ -199,8 +198,7 @@ void decodesXml() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(mockXml, UTF_8) .build(); @@ -223,8 +221,7 @@ class ParameterizedHolder { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.>emptyMap()) .body("", UTF_8) .build(); @@ -272,10 +269,9 @@ void decodeAnnotatedParameterizedTypes() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.>emptyMap()) - .body(template.body()) + .body(template.requestBody().map(this::bodyAsBytes).orElse(null)) .build(); new JAXBDecoder(new JAXBContextFactory.Builder().build()).decode(response, Box.class); @@ -288,8 +284,7 @@ void notFoundDecodesToEmpty() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.>emptyMap()) .build(); assertThat( @@ -312,8 +307,7 @@ void decodeThrowsExceptionWhenUnmarshallingFailsWithSetSchema() throws Exception Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(mockXml, UTF_8) .build(); @@ -341,8 +335,7 @@ void decodesIgnoringErrorsWithEventHandler() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(mockXml, UTF_8) .build(); @@ -394,6 +387,10 @@ 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 d355afb691..b1f98bd8a2 100644 --- a/jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java +++ b/jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java @@ -16,10 +16,12 @@ 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; @@ -72,8 +74,7 @@ private static String canonicalString(RequestTemplate input, String host) { canonicalRequest.append("host").append('\n'); // HexEncode(Hash(Payload)) - byte[] data = input.body(); - String bodyText = (data != null) ? new String(data, input.requestCharset()) : null; + String bodyText = input.requestBody().map(AWSSignatureVersion4::bodyAsUtf8String).orElse(null); if (bodyText != null) { canonicalRequest.append(hex(sha256(bodyText))); } else { @@ -82,6 +83,10 @@ 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' + @@ -116,7 +121,7 @@ public Request apply(RequestTemplate input) { if (!input.headers().isEmpty()) { throw new UnsupportedOperationException("headers not supported"); } - if (input.body() != null) { + if (input.requestBody().isPresent()) { 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 5ecdb94511..555012f4b8 100644 --- a/jaxrs2/src/main/java/feign/jaxrs2/JAXRSClient.java +++ b/jaxrs2/src/main/java/feign/jaxrs2/JAXRSClient.java @@ -19,7 +19,6 @@ 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; @@ -32,7 +31,7 @@ import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; -import javax.ws.rs.core.Variant; +import javax.ws.rs.core.StreamingOutput; /** * This module directs Feign's http requests to javax.ws.rs.client.Client . Ex: @@ -77,15 +76,11 @@ public feign.Response execute(feign.Request request, Options options) throws IOE .build(); } - 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 Entity createRequestEntity(feign.Request request) { + return request + .body() + .map(body -> Entity.entity((StreamingOutput) body::writeTo, mediaType(request.headers()))) + .orElse(null); } private Integer integerHeader(Response response, String header) { @@ -102,22 +97,16 @@ 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 MediaType mediaType(Map> headers) { - if (!headers.containsKey(HttpHeaders.CONTENT_TYPE)) return null; + private String mediaType(Map> headers) { + if (!headers.containsKey(HttpHeaders.CONTENT_TYPE)) return MediaType.APPLICATION_OCTET_STREAM; - return MediaType.valueOf(headers.get(HttpHeaders.CONTENT_TYPE).iterator().next()); + return 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 655bb7594a..1ef3cd6fb1 100644 --- a/json/src/main/java/feign/json/JsonEncoder.java +++ b/json/src/main/java/feign/json/JsonEncoder.java @@ -17,6 +17,7 @@ import static java.lang.String.format; +import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; @@ -58,7 +59,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(object.toString()); + template.body(Request.Body.of(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 becd040ee5..502b32eb9b 100644 --- a/json/src/test/java/feign/json/JsonCodecTest.java +++ b/json/src/test/java/feign/json/JsonCodecTest.java @@ -17,6 +17,7 @@ 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; @@ -27,6 +28,7 @@ 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; @@ -80,8 +82,9 @@ 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()).isNotNull(); - String json = new String(request.body()); + assertThat(request.body()).isPresent(); + String json = + assertDoesNotThrow(() -> request.body().get().writeToString(StandardCharsets.UTF_8)); 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 1635aa7f67..4147e3c469 100644 --- a/json/src/test/java/feign/json/JsonDecoderTest.java +++ b/json/src/test/java/feign/json/JsonDecoderTest.java @@ -26,7 +26,6 @@ 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; @@ -54,8 +53,7 @@ static void setUpClass() { Request.HttpMethod.GET, "/qwerty", Collections.emptyMap(), - "xyz".getBytes(StandardCharsets.UTF_8), - StandardCharsets.UTF_8, + Request.Body.of("xyz"), null); } diff --git a/json/src/test/java/feign/json/JsonEncoderTest.java b/json/src/test/java/feign/json/JsonEncoderTest.java index a8d2d1f360..d7cb951026 100644 --- a/json/src/test/java/feign/json/JsonEncoderTest.java +++ b/json/src/test/java/feign/json/JsonEncoderTest.java @@ -15,12 +15,14 @@ */ 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; @@ -49,20 +51,24 @@ void setUp() { void encodesArray() { new JsonEncoder().encode(jsonArray, JSONArray.class, requestTemplate); JSONAssert.assertEquals( - "[{\"a\":\"b\",\"c\":1},123]", new String(requestTemplate.body(), UTF_8), false); + "[{\"a\":\"b\",\"c\":1},123]", + requestTemplate.requestBody().map(this::bodyAsUtf8String).orElse(null), + false); } @Test void encodesObject() { new JsonEncoder().encode(jsonObject, JSONObject.class, requestTemplate); JSONAssert.assertEquals( - "{\"a\":\"b\",\"c\":1}", new String(requestTemplate.body(), UTF_8), false); + "{\"a\":\"b\",\"c\":1}", + requestTemplate.requestBody().map(this::bodyAsUtf8String).orElse(null), + false); } @Test void encodesNull() { new JsonEncoder().encode(null, JSONObject.class, new RequestTemplate()); - assertThat(requestTemplate.body()).isNull(); + assertThat(requestTemplate.requestBody()).isEmpty(); } @Test @@ -74,4 +80,8 @@ 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 011ee32ac2..cd846a74c5 100644 --- a/kotlin/src/test/kotlin/feign/kotlin/CoroutineFeignTest.kt +++ b/kotlin/src/test/kotlin/feign/kotlin/CoroutineFeignTest.kt @@ -19,6 +19,7 @@ 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 @@ -151,9 +152,9 @@ class CoroutineFeignTest { private val delegate = CoroutineFeign.builder() .decoder(DefaultDecoder()).encoder { `object`, bodyType, template -> if (`object` is Map<*, *>) { - template.body(Gson().toJson(`object`)) + template.body(Request.Body.of(Gson().toJson(`object`))) } else { - template.body(`object`.toString()) + template.body(Request.Body.of(`object`.toString())) } } diff --git a/micrometer/src/main/java/feign/micrometer/MeteredEncoder.java b/micrometer/src/main/java/feign/micrometer/MeteredEncoder.java index 2fb73d12f5..784aa86c3b 100644 --- a/micrometer/src/main/java/feign/micrometer/MeteredEncoder.java +++ b/micrometer/src/main/java/feign/micrometer/MeteredEncoder.java @@ -15,13 +15,19 @@ */ 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.*; +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 java.lang.reflect.Type; +import java.util.Collections; /** Wrap feign {@link Encoder} with metrics. */ public class MeteredEncoder implements Encoder { @@ -52,9 +58,11 @@ public void encode(Object object, Type bodyType, RequestTemplate template) createTimer(object, bodyType, template) .record(() -> encoder.encode(object, bodyType, template)); - if (template.body() != null) { - createSummary(object, bodyType, template).record(template.body().length); - } + template.headers().getOrDefault(CONTENT_LENGTH, Collections.emptySet()).stream() + .findFirst() + .ifPresent( + contentLength -> + createSummary(object, bodyType, template).record(Long.parseLong(contentLength))); } 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 50ed918e48..1d572875e3 100644 --- a/mock/src/main/java/feign/mock/RequestKey.java +++ b/mock/src/main/java/feign/mock/RequestKey.java @@ -18,10 +18,9 @@ import static feign.Util.UTF_8; import feign.Request; -import feign.Util; +import java.io.IOException; 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; @@ -36,8 +35,6 @@ public static class Builder { private RequestHeaders headers; - private Charset charset; - private byte[] body; private Builder(HttpMethod method, String url) { @@ -56,11 +53,6 @@ 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)); } @@ -85,7 +77,7 @@ public static RequestKey create(Request request) { private static String buildUrl(Request request) { try { - return URLDecoder.decode(request.url(), Util.UTF_8.name()); + return URLDecoder.decode(request.url(), UTF_8.name()); } catch (final UnsupportedEncodingException e) { throw new RuntimeException(e); } @@ -97,15 +89,12 @@ 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; } @@ -113,8 +102,19 @@ private RequestKey(Request request) { this.method = HttpMethod.valueOf(request.httpMethod().name()); this.url = buildUrl(request); this.headers = RequestHeaders.of(request.headers()); - this.charset = request.charset(); - this.body = request.body(); + this.body = + request + .body() + .filter(Request.Body::isRepeatable) + .map( + body -> { + try { + return body.writeToByteArray(); + } catch (IOException ignored) { + return null; + } + }) + .orElse(null); } public HttpMethod getMethod() { @@ -129,10 +129,6 @@ public RequestHeaders getHeaders() { return headers; } - public Charset getCharset() { - return charset; - } - public byte[] getBody() { return body; } @@ -171,10 +167,8 @@ public boolean equalsExtended(Object obj) { RequestKey other = (RequestKey) obj; boolean headersEqual = other.headers == null || headers == null || headers.equals(other.headers); - 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; + boolean bodyEqual = other.body == null || body == null || Arrays.equals(body, other.body); + return headersEqual && bodyEqual; } return false; } @@ -182,10 +176,7 @@ public boolean equalsExtended(Object obj) { @Override public String toString() { return String.format( - "Request [%s %s: %s headers and %s]", - method, - url, - headers == null ? "without" : "with " + headers, - charset == null ? "no charset" : "charset " + charset); + "Request [%s %s: %s headers]", + method, url, headers == null ? "without" : "with " + headers); } } diff --git a/mock/src/test/java/feign/mock/MockClientTest.java b/mock/src/test/java/feign/mock/MockClientTest.java index 77916f4194..3f14d92d90 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,7 +36,9 @@ 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; @@ -97,16 +99,6 @@ 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() @@ -119,7 +111,10 @@ void setup() throws IOException { HttpMethod.GET, "/repos/netflix/feign/contributors?client_id=7 7", new ByteArrayInputStream(data)) - .ok(postContributorKey, "{\"login\":\"velo\",\"contributions\":0}") + .ok( + HttpMethod.POST, + "/repos/netflix/feign/contributors", + "{\"login\":\"velo\",\"contributions\":0}") .noContent(HttpMethod.PATCH, "/repos/velo/feign-mock/contributors") .add( HttpMethod.GET, @@ -180,16 +175,6 @@ 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 @@ -200,14 +185,12 @@ 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); - assertThat(mockClient.verifyOne(testRequestKey).body()).isNotNull(); - byte[] body = mockClient.verifyOne(HttpMethod.POST, "/repos/netflix/feign/contributors").body(); - assertThat(body).isNotNull(); + Optional body = + mockClient.verifyOne(HttpMethod.POST, "/repos/netflix/feign/contributors").body(); + assertThat(body).isPresent(); - String message = new String(body); + String message = assertDoesNotThrow(() -> body.get().writeToString(StandardCharsets.UTF_8)); assertThat(message).contains("velo_at_github"); assertThat(message).contains("preposterous hacker"); @@ -222,7 +205,6 @@ void verifyNone() { testRequestKey = RequestKey.builder(HttpMethod.POST, "/repos/netflix/feign/contributors") - .charset(UTF_8) .headers( RequestHeaders.builder() .add("Content-Length", "55") @@ -243,7 +225,6 @@ 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 23d6d75fb2..9b03f3c0eb 100644 --- a/mock/src/test/java/feign/mock/RequestKeyTest.java +++ b/mock/src/test/java/feign/mock/RequestKeyTest.java @@ -18,7 +18,6 @@ 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; @@ -33,12 +32,7 @@ class RequestKeyTest { @BeforeEach void setUp() { RequestHeaders headers = RequestHeaders.builder().add("my-header", "val").build(); - requestKey = - RequestKey.builder(HttpMethod.GET, "a") - .headers(headers) - .charset(StandardCharsets.UTF_16) - .body("content") - .build(); + requestKey = RequestKey.builder(HttpMethod.GET, "a").headers(headers).body("content").build(); } @Test @@ -47,7 +41,6 @@ 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") @@ -56,20 +49,13 @@ void create() throws Exception { Map> map = new HashMap<>(); map.put("my-header", Arrays.asList("val")); Request request = - Request.create( - Request.HttpMethod.GET, - "a", - map, - "content".getBytes(StandardCharsets.UTF_8), - StandardCharsets.UTF_16); + Request.create(Request.HttpMethod.GET, "a", map, Request.Body.of("content"), null); 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 @@ -117,11 +103,7 @@ 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) - .charset(StandardCharsets.ISO_8859_1) - .build(); + RequestKey requestKey2 = RequestKey.builder(HttpMethod.GET, "a").headers(headers).build(); assertThat(requestKey.hashCode()).isEqualTo(requestKey2.hashCode()); assertThat(requestKey).isEqualTo(requestKey2); @@ -138,11 +120,7 @@ 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) - .charset(StandardCharsets.ISO_8859_1) - .build(); + RequestKey requestKey2 = RequestKey.builder(HttpMethod.GET, "a").headers(headers).build(); assertThat(requestKey.hashCode()).isEqualTo(requestKey2.hashCode()); assertThat(requestKey.equalsExtended(requestKey2)).isEqualTo(false); @@ -151,7 +129,7 @@ void equalsExtendedExtra() { @Test void testToString() throws Exception { assertThat(requestKey.toString()).startsWith("Request [GET a: "); - assertThat(requestKey.toString()).contains(" with my-header=[val] ", " UTF-16]"); + assertThat(requestKey.toString()).contains(" with my-header=[val] "); } @Test @@ -159,7 +137,6 @@ void toStringSimple() throws Exception { requestKey = RequestKey.builder(HttpMethod.GET, "a").build(); assertThat(requestKey.toString()).startsWith("Request [GET a: "); - assertThat(requestKey.toString()).contains(" without ", " no charset"); + assertThat(requestKey.toString()).contains(" without "); } } -// diff --git a/moshi/src/main/java/feign/moshi/MoshiEncoder.java b/moshi/src/main/java/feign/moshi/MoshiEncoder.java index b65f705e27..fdd7507741 100644 --- a/moshi/src/main/java/feign/moshi/MoshiEncoder.java +++ b/moshi/src/main/java/feign/moshi/MoshiEncoder.java @@ -17,6 +17,7 @@ import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; +import feign.Request; import feign.RequestTemplate; import feign.codec.Encoder; import feign.codec.JsonEncoder; @@ -41,6 +42,6 @@ public MoshiEncoder(Iterable> adapters) { @Override public void encode(Object object, Type bodyType, RequestTemplate template) { JsonAdapter jsonAdapter = moshi.adapter(bodyType).indent(" "); - template.body(jsonAdapter.toJson(object)); + template.body(Request.Body.of(jsonAdapter.toJson(object))); } } diff --git a/moshi/src/test/java/feign/moshi/MoshiDecoderTest.java b/moshi/src/test/java/feign/moshi/MoshiDecoderTest.java index 885a57b6df..7aabefc708 100644 --- a/moshi/src/test/java/feign/moshi/MoshiDecoderTest.java +++ b/moshi/src/test/java/feign/moshi/MoshiDecoderTest.java @@ -22,7 +22,6 @@ 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; @@ -60,8 +59,7 @@ class Zone extends LinkedHashMap { .reason("OK") .headers(Collections.emptyMap()) .request( - Request.create( - Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .body(zonesJson, UTF_8) .build(); @@ -106,8 +104,7 @@ void nullBodyDecodesToNull() throws Exception { .reason("OK") .headers(Collections.emptyMap()) .request( - Request.create( - Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .build(); assertThat(new MoshiDecoder().decode(response, String.class)).isNull(); } @@ -120,8 +117,7 @@ void emptyBodyDecodesToNull() throws Exception { .reason("OK") .headers(Collections.emptyMap()) .request( - Request.create( - Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .body(new byte[0]) .build(); assertThat(new MoshiDecoder().decode(response, String.class)).isNull(); @@ -136,8 +132,7 @@ void notFoundDecodesToEmpty() throws Exception { .reason("NOT FOUND") .headers(Collections.emptyMap()) .request( - Request.create( - Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .build(); assertThat((byte[]) new MoshiDecoder().decode(response, byte[].class)).isEmpty(); } @@ -158,8 +153,7 @@ void customDecoder() throws Exception { .reason("OK") .headers(Collections.emptyMap()) .request( - Request.create( - Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .body(zonesJson, UTF_8) .build(); @@ -181,8 +175,7 @@ void customObjectDecoder() throws Exception { .reason("OK") .headers(Collections.emptyMap()) .request( - Request.create( - Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + Request.create(Request.HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .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 25a8b7f1b1..d1a3f330c9 100644 --- a/okhttp/src/main/java/feign/okhttp/OkHttpClient.java +++ b/okhttp/src/main/java/feign/okhttp/OkHttpClient.java @@ -29,7 +29,15 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -import okhttp3.*; +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; /** * This module directs Feign's http requests to @@ -67,9 +75,6 @@ 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()); - } } } } @@ -78,21 +83,42 @@ static Request toOkHttpRequest(feign.Request input) { requestBuilder.addHeader("Accept", "*/*"); } - 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]; - } - } - - RequestBody body = inputBody != null ? RequestBody.create(mediaType, inputBody) : null; + 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(feignBody -> toRequestBody(feignBody, mediaType)) + .orElseGet(() -> RequestBody.create(new byte[0], mediaType)); + } + + static RequestBody toRequestBody(feign.Request.Body feignBody, MediaType mediaType) { + return new RequestBody() { + @Override + public MediaType contentType() { + return mediaType; + } + + @Override + public long contentLength() { + return feignBody.contentLength(); + } + + @Override + public boolean isOneShot() { + return !feignBody.isRepeatable(); + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + feignBody.writeTo(sink.outputStream()); + } + }; + } + private static feign.Response toFeignResponse(Response response, feign.Request request) throws IOException { return feign.Response.builder() diff --git a/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java b/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java index 79150b86b9..c03eb1ef4b 100644 --- a/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java +++ b/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java @@ -16,7 +16,6 @@ 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; @@ -528,7 +527,7 @@ void throwsFeignExceptionIncludingBody() throws Throwable { } catch (final FeignException e) { assertThat(e.getMessage()) .isEqualTo("timeout reading POST http://localhost:" + server.getPort() + "/"); - assertThat(e.contentUTF8()).isEqualTo("Request body"); + assertThat(e.contentUTF8()).isEmpty(); return; } fail(""); @@ -565,7 +564,7 @@ void whenReturnTypeIsResponseNoErrorHandling() throws Throwable { .status(302) .reason("Found") .headers(headers) - .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, null)) .body(new byte[0]) .build(); @@ -769,7 +768,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, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(new HashMap<>()) .build(); } @@ -1027,9 +1026,9 @@ static final class TestInterfaceAsyncBuilder { .encoder( (object, _, template) -> { if (object instanceof Map) { - template.body(new Gson().toJson(object)); + template.body(Request.Body.of(new Gson().toJson(object))); } else { - template.body(object.toString()); + template.body(Request.Body.of(object.toString())); } }); diff --git a/ribbon/src/main/java/feign/ribbon/LBClient.java b/ribbon/src/main/java/feign/ribbon/LBClient.java index 1062abb173..260b57df9e 100644 --- a/ribbon/src/main/java/feign/ribbon/LBClient.java +++ b/ribbon/src/main/java/feign/ribbon/LBClient.java @@ -122,15 +122,18 @@ 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()); - headers.put(Util.CONTENT_LENGTH, Collections.singletonList(String.valueOf(bodyLength))); + String contentLength = + request.body().map(body -> String.valueOf(body.contentLength())).orElse("0"); + headers.put(Util.CONTENT_LENGTH, Collections.singletonList(contentLength)); return Request.create( - request.httpMethod(), getUri().toASCIIString(), headers, body, request.charset()); + request.httpMethod(), + getUri().toASCIIString(), + headers, + request.body().orElse(null), + null); } Client client() { diff --git a/ribbon/src/test/java/feign/ribbon/LBClientTest.java b/ribbon/src/test/java/feign/ribbon/LBClientTest.java index ad29059cbe..b2086c6744 100644 --- a/ribbon/src/test/java/feign/ribbon/LBClientTest.java +++ b/ribbon/src/test/java/feign/ribbon/LBClientTest.java @@ -22,7 +22,6 @@ 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; @@ -48,8 +47,7 @@ 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, Charset.forName("utf-8")); + Request requestOrigin = Request.create(method, uri.toASCIIString(), headers, null, null); 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 21fdc29890..5b23f6eb1e 100644 --- a/sax/src/test/java/feign/sax/SAXDecoderTest.java +++ b/sax/src/test/java/feign/sax/SAXDecoderTest.java @@ -22,7 +22,6 @@ 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; @@ -77,7 +76,7 @@ private Response statusFailedResponse() { return Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.>emptyMap()) .body(statusFailed, UTF_8) .build(); @@ -89,8 +88,7 @@ void nullBodyDecodesToEmpty() throws Exception { Response.builder() .status(204) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.>emptyMap()) .build(); assertThat((byte[]) decoder.decode(response, byte[].class)).isEmpty(); @@ -103,8 +101,7 @@ void notFoundDecodesToEmpty() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .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 aac5a2ede5..bfb107a583 100644 --- a/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java +++ b/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java @@ -16,10 +16,12 @@ 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; @@ -72,8 +74,7 @@ private static String canonicalString(RequestTemplate input, String host) { canonicalRequest.append("host").append('\n'); // HexEncode(Hash(Payload)) - byte[] data = input.body(); - String bodyText = (data != null) ? new String(data, input.requestCharset()) : null; + String bodyText = input.requestBody().map(AWSSignatureVersion4::bodyAsUtf8String).orElse(null); if (bodyText != null) { canonicalRequest.append(hex(sha256(bodyText))); } else { @@ -82,6 +83,10 @@ 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' + @@ -116,7 +121,7 @@ public Request apply(RequestTemplate input) { if (!input.headers().isEmpty()) { throw new UnsupportedOperationException("headers not supported"); } - if (input.body() != null) { + if (input.requestBody().isPresent()) { 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 0a1f235ef9..566a736438 100644 --- a/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java +++ b/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java @@ -21,7 +21,6 @@ import feign.Request.HttpMethod; import feign.RequestTemplate; import feign.Response; -import feign.Util; import java.util.Collection; import java.util.Collections; import org.junit.jupiter.api.Test; @@ -42,7 +41,7 @@ public class Slf4jLoggerTest { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.>emptyMap()) .body(new byte[0]) .build(); diff --git a/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java b/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java index 709b7f2a13..31bc768304 100644 --- a/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java +++ b/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java @@ -15,6 +15,7 @@ */ package feign.soap; +import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; @@ -32,7 +33,11 @@ import java.nio.charset.StandardCharsets; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.*; +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.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.w3c.dom.Document; @@ -131,7 +136,7 @@ public void encode(Object object, Type bodyType, RequestTemplate template) { } else { soapMessage.writeTo(bos); } - template.body(bos.toString()); + template.body(Request.Body.of(bos.toByteArray())); } 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 0cbb94be42..8634ff9815 100644 --- a/soap-jakarta/src/test/java/feign/soap/SOAPCodecTest.java +++ b/soap-jakarta/src/test/java/feign/soap/SOAPCodecTest.java @@ -17,14 +17,13 @@ import static feign.Util.UTF_8; import static feign.assertj.FeignAssertions.assertThat; -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.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; @@ -254,8 +253,7 @@ void decodesSoap() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(mockSoapEnvelop, UTF_8) .build(); @@ -290,8 +288,7 @@ void decodesSoapWithSchemaOnEnvelope() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(mockSoapEnvelop, UTF_8) .build(); @@ -328,8 +325,7 @@ void decodesSoap1_2Protocol() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(mockSoapEnvelop, UTF_8) .build(); @@ -353,8 +349,7 @@ class ParameterizedHolder { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body( """ @@ -404,10 +399,9 @@ void decodeAnnotatedParameterizedTypes() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) - .body(template.body()) + .body(template.requestBody().map(this::bodyAsBytes).orElse(null)) .build(); new SOAPDecoder(new JAXBContextFactory.Builder().build()).decode(response, Box.class); @@ -420,8 +414,7 @@ void notFoundDecodesToNull() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .build(); assertThat( @@ -466,6 +459,10 @@ 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 c94f83b16a..85cd3979a5 100644 --- a/soap-jakarta/src/test/java/feign/soap/SOAPFaultDecoderTest.java +++ b/soap-jakarta/src/test/java/feign/soap/SOAPFaultDecoderTest.java @@ -23,7 +23,6 @@ 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; @@ -50,8 +49,7 @@ void soapDecoderThrowsSOAPFaultException() throws IOException { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(getResourceBytes("/samples/SOAP_1_2_FAULT.xml")) .build(); @@ -73,8 +71,7 @@ void errorDecoderReturnsSOAPFaultException() throws IOException { Response.builder() .status(400) .reason("BAD REQUEST") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(getResourceBytes("/samples/SOAP_1_1_FAULT.xml")) .build(); @@ -91,8 +88,7 @@ void errorDecoderReturnsFeignExceptionOn503Status() throws IOException { Response.builder() .status(503) .reason("Service Unavailable") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body("Service Unavailable", UTF_8) .build(); @@ -123,8 +119,7 @@ void errorDecoderReturnsFeignExceptionOnEmptyFault() throws IOException { Response.builder() .status(500) .reason("Internal Server Error") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .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 24f10da917..711c44171f 100644 --- a/soap/src/main/java/feign/soap/SOAPEncoder.java +++ b/soap/src/main/java/feign/soap/SOAPEncoder.java @@ -15,6 +15,7 @@ */ package feign.soap; +import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; @@ -135,7 +136,7 @@ public void encode(Object object, Type bodyType, RequestTemplate template) { } else { soapMessage.writeTo(bos); } - template.body(new String(bos.toByteArray())); + template.body(Request.Body.of(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 002a0e33b6..59d38432da 100644 --- a/soap/src/test/java/feign/soap/SOAPCodecTest.java +++ b/soap/src/test/java/feign/soap/SOAPCodecTest.java @@ -17,14 +17,13 @@ import static feign.Util.UTF_8; import static feign.assertj.FeignAssertions.assertThat; -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.Request.HttpMethod; import feign.RequestTemplate; import feign.Response; -import feign.Util; import feign.codec.Encoder; import feign.jaxb.JAXBContextFactory; import java.lang.reflect.Type; @@ -254,8 +253,7 @@ void decodesSoap() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(mockSoapEnvelop, UTF_8) .build(); @@ -290,8 +288,7 @@ void decodesSoapWithSchemaOnEnvelope() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(mockSoapEnvelop, UTF_8) .build(); @@ -328,8 +325,7 @@ void decodesSoap1_2Protocol() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(mockSoapEnvelop, UTF_8) .build(); @@ -353,8 +349,7 @@ class ParameterizedHolder { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body( """ @@ -414,10 +409,9 @@ void decodeAnnotatedParameterizedTypes() throws Exception { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) - .body(template.body()) + .body(template.requestBody().map(this::bodyAsBytes).orElse(null)) .build(); new SOAPDecoder(new JAXBContextFactory.Builder().build()).decode(response, Box.class); @@ -430,8 +424,7 @@ void notFoundDecodesToNull() throws Exception { Response.builder() .status(404) .reason("NOT FOUND") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .build(); assertThat( @@ -476,6 +469,10 @@ 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 15d3f76696..810f173f11 100644 --- a/soap/src/test/java/feign/soap/SOAPFaultDecoderTest.java +++ b/soap/src/test/java/feign/soap/SOAPFaultDecoderTest.java @@ -23,7 +23,6 @@ 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; @@ -43,8 +42,7 @@ void soapDecoderThrowsSOAPFaultException() throws IOException { Response.builder() .status(200) .reason("OK") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(getResourceBytes("/samples/SOAP_1_2_FAULT.xml")) .build(); @@ -65,8 +63,7 @@ void errorDecoderReturnsSOAPFaultException() throws IOException { Response.builder() .status(400) .reason("BAD REQUEST") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body(getResourceBytes("/samples/SOAP_1_1_FAULT.xml")) .build(); @@ -83,8 +80,7 @@ void errorDecoderReturnsFeignExceptionOn503Status() throws IOException { Response.builder() .status(503) .reason("Service Unavailable") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .headers(Collections.emptyMap()) .body("Service Unavailable", UTF_8) .build(); @@ -115,8 +111,7 @@ void errorDecoderReturnsFeignExceptionOnEmptyFault() throws IOException { Response.builder() .status(500) .reason("Internal Server Error") - .request( - Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, null)) .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 4f0277d70e..7788815757 100755 --- a/spring/src/test/java/feign/spring/SpringContractTest.java +++ b/spring/src/test/java/feign/spring/SpringContractTest.java @@ -16,6 +16,7 @@ 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; @@ -181,7 +182,8 @@ void nonRequiredRequestBodyIsNull() { resource.checkWithNonRequiredRequestBody(null); Request request = mockClient.verifyOne(HttpMethod.POST, "/health/withNonRequiredRequestBody"); - assertThat(request.requestTemplate().body()).asString().isEqualTo("null"); + assertThat(request.requestTemplate().requestBody().map(this::bodyAsUtf8String)) + .contains("null"); } @Test @@ -191,7 +193,8 @@ void nonRequiredRequestBodyIsObject() { resource.checkWithNonRequiredRequestBody(object); Request request = mockClient.verifyOne(HttpMethod.POST, "/health/withNonRequiredRequestBody"); - assertThat(request.requestTemplate().body()).asString().contains("\"name\" : \"hello\""); + assertThat(request.requestTemplate().requestBody().map(this::bodyAsUtf8String).orElse(null)) + .contains("\"name\" : \"hello\""); } @Test @@ -281,6 +284,10 @@ 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/vertx/feign-vertx/src/main/java/feign/VertxFeign.java b/vertx/feign-vertx/src/main/java/feign/VertxFeign.java index 09a6264ebe..24a24cef59 100644 --- a/vertx/feign-vertx/src/main/java/feign/VertxFeign.java +++ b/vertx/feign-vertx/src/main/java/feign/VertxFeign.java @@ -28,6 +28,7 @@ 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; @@ -94,6 +95,7 @@ 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; @@ -122,6 +124,11 @@ public Builder invocationHandlerFactory( throw new UnsupportedOperationException(); } + public Builder vertx(final Vertx vertx) { + this.vertx = vertx; + return this; + } + /** * Sets a vertx WebClient. * @@ -370,10 +377,12 @@ 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(webClient, timeout, requestPreProcessor); + final VertxHttpClient client = + new VertxHttpClient(vertx, 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 new file mode 100644 index 0000000000..e124976ad7 --- /dev/null +++ b/vertx/feign-vertx/src/main/java/feign/vertx/OutputToReadStream.java @@ -0,0 +1,308 @@ +/* + * 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 0b196b6ffb..0a00bd2adb 100644 --- a/vertx/feign-vertx/src/main/java/feign/vertx/VertxHttpClient.java +++ b/vertx/feign-vertx/src/main/java/feign/vertx/VertxHttpClient.java @@ -44,6 +44,7 @@ */ @SuppressWarnings("unused") public final class VertxHttpClient { + private final Vertx vertx; private final WebClient webClient; private final long timeout; private final UnaryOperator> requestPreProcessor; @@ -56,12 +57,15 @@ 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; @@ -85,9 +89,25 @@ public Future execute(final Request request) { } final Future> responseFuture = - request.body() != null - ? httpClientRequest.sendBuffer(Buffer.buffer(request.body())) - : httpClientRequest.send(); + 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); 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 dae2645f3e..0ecffbdfe6 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java @@ -81,6 +81,7 @@ void http11NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { HelloServiceAPI client = VertxFeign.builder() + .vertx(vertx) .webClient(webClient) .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) @@ -101,6 +102,7 @@ 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 fae9a70e9d..88dab3713b 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java @@ -38,6 +38,7 @@ 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 39a43a8adb..69e3d5dfd8 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/QueryMapEncoderTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/QueryMapEncoderTest.java @@ -55,6 +55,7 @@ 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 48f46b4b49..e816622474 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/RawContractTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/RawContractTest.java @@ -45,6 +45,7 @@ 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 87850cb9e3..2d88afa6c9 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/RequestPreProcessorTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/RequestPreProcessorTest.java @@ -47,6 +47,7 @@ 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 b294d99363..89691c9d5a 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/RetryingTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/RetryingTest.java @@ -48,6 +48,7 @@ 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 29e7efa6e1..5bd97dd4a5 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/TimeoutHandlingTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/TimeoutHandlingTest.java @@ -48,6 +48,7 @@ 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 eca7765385..005568436b 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpClientTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpClientTest.java @@ -65,6 +65,7 @@ void createClient(Vertx vertx) { client = VertxFeign.builder() + .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .logger(new Slf4jLogger()) @@ -217,6 +218,7 @@ 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)) @@ -304,6 +306,23 @@ 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) @@ -318,6 +337,7 @@ 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 626376a347..21eefbe9ca 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpOptionsTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpOptionsTest.java @@ -65,6 +65,7 @@ void httpClientOptions(Vertx vertx, VertxTestContext testContext) { IcecreamServiceApi client = VertxFeign.builder() + .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .logger(new Slf4jLogger()) @@ -86,6 +87,7 @@ 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 15669e2c7f..3224f6462d 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,6 +84,7 @@ void http11NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { HelloServiceAPI client = VertxFeign.builder() + .vertx(vertx) .webClient(webClient) .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) @@ -104,6 +105,7 @@ 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 207ae8a89e..bb6edaf4a8 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,6 +36,7 @@ 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 39a43a8adb..69e3d5dfd8 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,6 +55,7 @@ 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 48f46b4b49..e816622474 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,6 +45,7 @@ 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 87850cb9e3..2d88afa6c9 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,6 +47,7 @@ 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 b294d99363..89691c9d5a 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,6 +48,7 @@ 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 29e7efa6e1..5bd97dd4a5 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,6 +48,7 @@ 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 eca7765385..005568436b 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,6 +65,7 @@ void createClient(Vertx vertx) { client = VertxFeign.builder() + .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .logger(new Slf4jLogger()) @@ -217,6 +218,7 @@ 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)) @@ -304,6 +306,23 @@ 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) @@ -318,6 +337,7 @@ 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 0e83d3ea62..2ca7a54cb6 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,6 +64,7 @@ void httpClientOptions(Vertx vertx, VertxTestContext testContext) { IcecreamServiceApi client = VertxFeign.builder() + .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .logger(new Slf4jLogger()) @@ -85,6 +86,7 @@ void requestOptions(Vertx vertx, VertxTestContext testContext) { IcecreamServiceApi client = VertxFeign.builder() + .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .logger(new Slf4jLogger()) From b03c25b1eb8a121a6f910ec6804bf53d641dfa4f Mon Sep 17 00:00:00 2001 From: Yevhen Vasyliev Date: Mon, 18 May 2026 09:04:41 +0300 Subject: [PATCH 2/7] chore: remove `ThrowingConsumer` Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com> --- .../java/feign/utils/ThrowingConsumer.java | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 core/src/main/java/feign/utils/ThrowingConsumer.java diff --git a/core/src/main/java/feign/utils/ThrowingConsumer.java b/core/src/main/java/feign/utils/ThrowingConsumer.java deleted file mode 100644 index 924f8bc705..0000000000 --- a/core/src/main/java/feign/utils/ThrowingConsumer.java +++ /dev/null @@ -1,34 +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.utils; - -/** - * A functional interface similar to {@link java.util.function.Consumer} that allows for checked - * exceptions. - * - * @param the type of the input to the operation - * @param the type of exception that may be thrown - */ -@FunctionalInterface -public interface ThrowingConsumer { - /** - * Performs this operation on the given argument. - * - * @param t the input argument - * @throws E if an exception occurs during the operation - */ - void accept(T t) throws E; -} From cd643ea1ba2d6dadbdde00d3b005e96ccf1c0217 Mon Sep 17 00:00:00 2001 From: Yevhen Vasyliev Date: Mon, 18 May 2026 11:09:00 +0300 Subject: [PATCH 3/7] chore: remove unused `FeignBodyEntity`; improve Javadoc redability Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com> --- core/src/main/java/feign/Request.java | 5 ++- .../feign/httpclient/ApacheHttpClient.java | 38 ------------------- .../main/java/feign/okhttp/OkHttpClient.java | 10 ++--- 3 files changed, 8 insertions(+), 45 deletions(-) diff --git a/core/src/main/java/feign/Request.java b/core/src/main/java/feign/Request.java index 715c566c73..53c5ce82e7 100644 --- a/core/src/main/java/feign/Request.java +++ b/core/src/main/java/feign/Request.java @@ -428,7 +428,7 @@ public RequestTemplate requestTemplate() { public interface Body { /** * Creates a new {@link Body} instance from the provided string content. It's assumed that the - * content was constructed using {@link StandardCharsets#UTF_8} encoding. + * content was constructed using {@link StandardCharsets#UTF_8} charset. * * @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 @@ -438,7 +438,8 @@ static Body of(String content) { } /** - * Creates a new {@link Body} instance from the provided byte array. + * Creates a new {@link Body} instance from the provided byte array. It's assumed that the byte + * array can be converted to a string using {@link StandardCharsets#UTF_8} charset. * * @param content the byte array representing the body content * @return a new {@link Body} instance diff --git a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java index 8f023c1e5d..45e627bf27 100644 --- a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java +++ b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java @@ -272,42 +272,4 @@ public void close() throws IOException { } }; } - - private static final class FeignBodyEntity extends AbstractHttpEntity { - - private final Request.Body body; - private final long contentLength; - - private FeignBodyEntity(Request.Body body, ContentType contentType, long contentLength) { - this.body = body; - this.contentLength = contentLength; - setContentType(contentType.toString()); - setChunked(contentLength < 0); - } - - @Override - public long getContentLength() { - return 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(); - } - } } diff --git a/okhttp/src/main/java/feign/okhttp/OkHttpClient.java b/okhttp/src/main/java/feign/okhttp/OkHttpClient.java index d1a3f330c9..ee582ad7a4 100644 --- a/okhttp/src/main/java/feign/okhttp/OkHttpClient.java +++ b/okhttp/src/main/java/feign/okhttp/OkHttpClient.java @@ -91,11 +91,11 @@ static Request toOkHttpRequest(feign.Request input) { static RequestBody toRequestBody(feign.Request request, MediaType mediaType) { return request .body() - .map(feignBody -> toRequestBody(feignBody, mediaType)) + .map(body -> toRequestBody(body, mediaType)) .orElseGet(() -> RequestBody.create(new byte[0], mediaType)); } - static RequestBody toRequestBody(feign.Request.Body feignBody, MediaType mediaType) { + static RequestBody toRequestBody(feign.Request.Body body, MediaType mediaType) { return new RequestBody() { @Override public MediaType contentType() { @@ -104,17 +104,17 @@ public MediaType contentType() { @Override public long contentLength() { - return feignBody.contentLength(); + return body.contentLength(); } @Override public boolean isOneShot() { - return !feignBody.isRepeatable(); + return !body.isRepeatable(); } @Override public void writeTo(BufferedSink sink) throws IOException { - feignBody.writeTo(sink.outputStream()); + body.writeTo(sink.outputStream()); } }; } From 94b13e83f155acd609e5af86ad250713a4f6b216 Mon Sep 17 00:00:00 2001 From: Yevhen Vasyliev Date: Mon, 18 May 2026 11:40:14 +0300 Subject: [PATCH 4/7] docs: add `@apiNote` to request body factory methods Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com> --- core/src/main/java/feign/Request.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/feign/Request.java b/core/src/main/java/feign/Request.java index 53c5ce82e7..a0afea25cf 100644 --- a/core/src/main/java/feign/Request.java +++ b/core/src/main/java/feign/Request.java @@ -427,22 +427,24 @@ public RequestTemplate requestTemplate() { @Experimental public interface Body { /** - * Creates a new {@link Body} instance from the provided string content. It's assumed that the - * content was constructed using {@link StandardCharsets#UTF_8} charset. + * 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. It's assumed that the byte - * array can be converted to a string using {@link StandardCharsets#UTF_8} charset. + * 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); From 994d5fb625f508365d5891612ce170edd6aeba03 Mon Sep 17 00:00:00 2001 From: Yevhen Vasyliev Date: Tue, 19 May 2026 06:25:36 +0300 Subject: [PATCH 5/7] fix: propagate `IOException` from async IO operation Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com> --- .../java/feign/http2client/Http2Client.java | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/java11/src/main/java/feign/http2client/Http2Client.java b/java11/src/main/java/feign/http2client/Http2Client.java index c169149816..e3cbd394ce 100644 --- a/java11/src/main/java/feign/http2client/Http2Client.java +++ b/java11/src/main/java/feign/http2client/Http2Client.java @@ -15,8 +15,6 @@ */ package feign.http2client; -import static feign.Util.*; - import feign.AsyncClient; import feign.Client; import feign.Request; @@ -24,6 +22,7 @@ import feign.Request.ProtocolVersion; import feign.Response; import feign.Util; + import java.io.IOException; import java.io.InputStream; import java.io.PipedInputStream; @@ -55,11 +54,17 @@ import java.util.TreeSet; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.stream.Collectors; import java.util.zip.GZIPInputStream; import java.util.zip.InflaterInputStream; +import static feign.Util.CONTENT_ENCODING; +import static feign.Util.ENCODING_DEFLATE; +import static feign.Util.ENCODING_GZIP; +import static feign.Util.enumForName; + public class Http2Client implements Client, AsyncClient { private final HttpClient client; @@ -239,14 +244,15 @@ private BodyPublisher createBodyPublisher(Request.Body body) { BodyPublisher publisher = BodyPublishers.ofInputStream( () -> { - PipedInputStream inputStream = new PipedInputStream(); + PropagatingPipedInputStream inputStream = new PropagatingPipedInputStream(); try { PipedOutputStream outputStream = new PipedOutputStream(inputStream); CompletableFuture.runAsync( () -> { try (outputStream) { body.writeTo(outputStream); - } catch (IOException ignored) { + } catch (IOException e) { + inputStream.setException(e); } }); return inputStream; @@ -300,4 +306,35 @@ private String[] asString(Map> headers) { .flatMap(List::stream)) .toArray(String[]::new); } + + 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); + } + } + } } From 0efde040f51394b3bd9fd5d661f92f391d0556ed Mon Sep 17 00:00:00 2001 From: Yevhen Vasyliev Date: Tue, 19 May 2026 07:11:24 +0300 Subject: [PATCH 6/7] feat: make Executor injectable Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com> --- .../feign/hc5/AsyncApacheHttp5Client.java | 39 ++++++++++++------- .../java/feign/http2client/Http2Client.java | 19 +++++++-- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/hc5/src/main/java/feign/hc5/AsyncApacheHttp5Client.java b/hc5/src/main/java/feign/hc5/AsyncApacheHttp5Client.java index 82bc294cae..9237d3db72 100644 --- a/hc5/src/main/java/feign/hc5/AsyncApacheHttp5Client.java +++ b/hc5/src/main/java/feign/hc5/AsyncApacheHttp5Client.java @@ -15,23 +15,11 @@ */ package feign.hc5; -import static feign.Util.enumForName; - import feign.AsyncClient; import feign.Request; import feign.Request.Options; import feign.Response; import feign.Util; -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.Optional; -import java.util.concurrent.CompletableFuture; 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; @@ -54,6 +42,22 @@ import org.apache.hc.core5.net.URLEncodedUtils; import org.apache.hc.core5.util.Timeout; +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.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.ForkJoinPool; + +import static feign.Util.enumForName; + /** * This module directs Feign's http requests to Apache's * HttpClient 5. Ex. @@ -70,12 +74,19 @@ public final class AsyncApacheHttp5Client implements AsyncClient { try { requestProducer.blockWaiting().execute(); diff --git a/java11/src/main/java/feign/http2client/Http2Client.java b/java11/src/main/java/feign/http2client/Http2Client.java index e3cbd394ce..5527a212c1 100644 --- a/java11/src/main/java/feign/http2client/Http2Client.java +++ b/java11/src/main/java/feign/http2client/Http2Client.java @@ -54,6 +54,8 @@ import java.util.TreeSet; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.ForkJoinPool; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.stream.Collectors; @@ -69,6 +71,8 @@ public class Http2Client implements Client, AsyncClient { private final HttpClient client; + private final Executor executor; + private final Map> clients = new ConcurrentHashMap<>(); /** @@ -91,12 +95,21 @@ public Http2Client() { .build()); } + public Http2Client(HttpClient client) { + this(client, ForkJoinPool.commonPool()); + } + public Http2Client(Options options) { - this(newClientBuilder(options).version(Version.HTTP_2).build()); + this(options, ForkJoinPool.commonPool()); } - public Http2Client(HttpClient client) { + public Http2Client(Options options, Executor executor) { + this(newClientBuilder(options).version(Version.HTTP_2).build(), executor); + } + + public Http2Client(HttpClient client, Executor executor) { this.client = Util.checkNotNull(client, "HttpClient must not be null"); + this.executor = Util.checkNotNull(executor, "Executor must not be null"); } @Override @@ -247,7 +260,7 @@ private BodyPublisher createBodyPublisher(Request.Body body) { PropagatingPipedInputStream inputStream = new PropagatingPipedInputStream(); try { PipedOutputStream outputStream = new PipedOutputStream(inputStream); - CompletableFuture.runAsync( + executor.execute( () -> { try (outputStream) { body.writeTo(outputStream); From 16f10d45503bc963886744e13bac143548c1cce0 Mon Sep 17 00:00:00 2001 From: Yevhen Vasyliev Date: Tue, 19 May 2026 09:44:34 +0300 Subject: [PATCH 7/7] chore: code format Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com> --- .../feign/hc5/AsyncApacheHttp5Client.java | 31 +++++++++---------- .../java/feign/http2client/Http2Client.java | 11 +++---- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/hc5/src/main/java/feign/hc5/AsyncApacheHttp5Client.java b/hc5/src/main/java/feign/hc5/AsyncApacheHttp5Client.java index 9237d3db72..c555d60869 100644 --- a/hc5/src/main/java/feign/hc5/AsyncApacheHttp5Client.java +++ b/hc5/src/main/java/feign/hc5/AsyncApacheHttp5Client.java @@ -15,11 +15,26 @@ */ package feign.hc5; +import static feign.Util.enumForName; + import feign.AsyncClient; import feign.Request; import feign.Request.Options; import feign.Response; import feign.Util; +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.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.ForkJoinPool; 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; @@ -42,22 +57,6 @@ import org.apache.hc.core5.net.URLEncodedUtils; import org.apache.hc.core5.util.Timeout; -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.concurrent.CompletableFuture; -import java.util.concurrent.Executor; -import java.util.concurrent.ForkJoinPool; - -import static feign.Util.enumForName; - /** * This module directs Feign's http requests to Apache's * HttpClient 5. Ex. diff --git a/java11/src/main/java/feign/http2client/Http2Client.java b/java11/src/main/java/feign/http2client/Http2Client.java index 5527a212c1..75702dc157 100644 --- a/java11/src/main/java/feign/http2client/Http2Client.java +++ b/java11/src/main/java/feign/http2client/Http2Client.java @@ -15,6 +15,11 @@ */ 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 feign.AsyncClient; import feign.Client; import feign.Request; @@ -22,7 +27,6 @@ import feign.Request.ProtocolVersion; import feign.Response; import feign.Util; - import java.io.IOException; import java.io.InputStream; import java.io.PipedInputStream; @@ -62,11 +66,6 @@ import java.util.zip.GZIPInputStream; import java.util.zip.InflaterInputStream; -import static feign.Util.CONTENT_ENCODING; -import static feign.Util.ENCODING_DEFLATE; -import static feign.Util.ENCODING_GZIP; -import static feign.Util.enumForName; - public class Http2Client implements Client, AsyncClient { private final HttpClient client;