From cef5e55b6256f7d07336d74b65ff3753650ed544 Mon Sep 17 00:00:00 2001 From: Rishabh Srivastava Date: Wed, 13 May 2026 15:38:56 +0000 Subject: [PATCH 01/51] do not unwrap checked and undeclared exceptions --- .../java/feign/SynchronousMethodHandler.java | 18 ++++++++- core/src/test/java/feign/FeignTest.java | 37 +++++++++++++++++++ .../feign/httpclient/ApacheHttpClient.java | 8 +++- .../httpclient/ApacheHttpClientTest.java | 17 +++++++++ 4 files changed, 77 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/feign/SynchronousMethodHandler.java b/core/src/main/java/feign/SynchronousMethodHandler.java index 244fe90f5d..6680fc5aa3 100644 --- a/core/src/main/java/feign/SynchronousMethodHandler.java +++ b/core/src/main/java/feign/SynchronousMethodHandler.java @@ -24,6 +24,7 @@ import feign.interceptor.Invocation; import feign.interceptor.MethodInterceptor; import java.io.IOException; +import java.lang.reflect.Method; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; @@ -75,7 +76,12 @@ private Object runWithRetry(Invocation invocation, Options options) throws Throw retryer.continueOrPropagate(e); } catch (RetryableException th) { Throwable cause = th.getCause(); - if (methodHandlerConfiguration.getPropagationPolicy() == UNWRAP && cause != null) { + if (methodHandlerConfiguration.getPropagationPolicy() == UNWRAP + && cause != null + && (cause instanceof RuntimeException + || cause instanceof Error + || isDeclaredCheckedException( + cause, methodHandlerConfiguration.getMetadata().method()))) { throw cause; } else { throw th; @@ -160,6 +166,16 @@ Options findOptions(Object[] argv) { .getMethodOptions(methodHandlerConfiguration.getMetadata().method().getName())); } + private static boolean isDeclaredCheckedException(Throwable cause, Method method) { + Class[] exceptionTypes = method.getExceptionTypes(); + for (Class exType : exceptionTypes) { + if (exType.isAssignableFrom(cause.getClass())) { + return true; + } + } + return false; + } + static class Factory implements MethodHandler.Factory { private final Client client; diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 748bfe1ad5..f30c069f50 100755 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -50,6 +50,7 @@ import feign.querymap.FieldQueryMapEncoder; import java.io.IOException; import java.lang.reflect.Type; +import java.net.ProtocolException; import java.net.URI; import java.time.Clock; import java.time.Instant; @@ -708,6 +709,39 @@ void throwsRetryableExceptionIfNoUnderlyingCause() throws Exception { assertThat(exception.getMessage()).contains(message); } + @Test + void doesNotUnwrapUndeclaredCheckedCauseWhenPropagationPolicyIsUnwrap() { + TestInterface api = + Feign.builder() + .exceptionPropagationPolicy(UNWRAP) + .retryer(new DefaultRetryer(1, 1, 1)) + .client( + (_, _) -> { + throw new ProtocolException("missing Location header for redirect"); + }) + .target(TestInterface.class, "http://localhost:" + server.getPort()); + + RetryableException exception = assertThrows(RetryableException.class, () -> api.post()); + assertThat(exception.getMessage()).contains("missing Location header for redirect"); + } + + @Test + void unwrapDeclaredCheckedCauseWhenPropagationPolicyIsUnwrap() { + TestInterface api = + Feign.builder() + .exceptionPropagationPolicy(UNWRAP) + .retryer(new DefaultRetryer(1, 1, 1)) + .client( + (_, _) -> { + throw new ProtocolException("missing Location header for redirect"); + }) + .target(TestInterface.class, "http://localhost:" + server.getPort()); + + ProtocolException exception = + assertThrows(ProtocolException.class, () -> api.postThrowsProtocolException()); + assertThat(exception.getMessage()).contains("missing Location header for redirect"); + } + @Test void whenReturnTypeIsResponseNoErrorHandling() { Map> headers = new LinkedHashMap<>(); @@ -1234,6 +1268,9 @@ interface TestInterface { @RequestLine("POST /") String post() throws TestInterfaceException; + @RequestLine("POST /") + String postThrowsProtocolException() throws ProtocolException; + @RequestLine("POST /") @Body( "%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\":" diff --git a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java index 010c229cb7..5ab2ba459e 100644 --- a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java +++ b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java @@ -81,8 +81,12 @@ public Response execute(Request request, Request.Options options) throws IOExcep } catch (URISyntaxException e) { throw new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", e); } - HttpResponse httpResponse = client.execute(httpUriRequest); - return toFeignResponse(httpResponse, request); + try { + HttpResponse httpResponse = client.execute(httpUriRequest); + return toFeignResponse(httpResponse, request); + } catch (Exception e) { + throw new IOException(e); + } } HttpUriRequest toHttpUriRequest(Request request, Request.Options options) diff --git a/httpclient/src/test/java/feign/httpclient/ApacheHttpClientTest.java b/httpclient/src/test/java/feign/httpclient/ApacheHttpClientTest.java index 3506aae5be..9ad3406f74 100644 --- a/httpclient/src/test/java/feign/httpclient/ApacheHttpClientTest.java +++ b/httpclient/src/test/java/feign/httpclient/ApacheHttpClientTest.java @@ -23,6 +23,8 @@ import feign.Feign.Builder; import feign.FeignException; import feign.Request.Options; +import feign.RetryableException; +import feign.Retryer; import feign.client.AbstractClientTest; import feign.jaxrs.JAXRSContract; import java.nio.charset.StandardCharsets; @@ -89,6 +91,21 @@ void notFollowRedirectIsRespected() throws InterruptedException { assertThat(server.takeRequest().getPath()).isEqualTo("/withOptions"); } + @Test + void redirectWithoutLocationHeader() { + JaxRsTestInterface api = + Feign.builder() + .contract(new JAXRSContract()) + .retryer(Retryer.NEVER_RETRY) + .client(new ApacheHttpClient(HttpClientBuilder.create().build())) + .target(JaxRsTestInterface.class, "http://localhost:" + server.getPort()); + + server.enqueue(new MockResponse().setResponseCode(303)); + RetryableException exception = + assertThrows(RetryableException.class, () -> api.withoutBody("foo")); + assertThat(exception.getMessage()).contains("org.apache.http.client.ClientProtocolException"); + } + private JaxRsTestInterface buildTestInterface() { return Feign.builder() .contract(new JAXRSContract()) From 16cf31218184b978bb5b9115098d77c1357e1d33 Mon Sep 17 00:00:00 2001 From: seonwoojung Date: Thu, 11 Jun 2026 20:17:14 +0900 Subject: [PATCH 02/51] Open observation scope and stop observation on non-FeignException errors (#3407) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `MicrometerObservationCapability` started an `Observation` but never opened its `Scope`, so the new observation was not the current one while the call ran. Downstream `ObservationHandler`s — most importantly, tracing handlers that copy trace/span ids into the MDC — saw the parent observation instead of the Feign one, so log lines for the outgoing call were tagged with the wrong span id. The capability also caught only `FeignException`. Underlying clients (JDK `HttpURLConnection`, Apache HC5, OkHttp, …) throw client-specific exceptions — `SocketTimeoutException`, `HttpHostConnectException`, `IOException` — directly, never wrapped as `FeignException`. Those slipped past the `catch`, so the observation was never stopped: the error was not recorded, the timer was never committed, and the observation leaked. Both calls now open the observation as a `Scope` while the underlying client runs, catch any `Throwable` to record the error and stop the observation, and unwrap `CompletionException` on the async path so the recorded error is the cause rather than the wrapper. Fixes #2406 Fixes #2566 --- .../MicrometerObservationCapability.java | 39 ++- .../MicrometerObservationCapabilityTest.java | 302 ++++++++++++++++++ 2 files changed, 325 insertions(+), 16 deletions(-) create mode 100644 micrometer/src/test/java/feign/micrometer/MicrometerObservationCapabilityTest.java diff --git a/micrometer/src/main/java/feign/micrometer/MicrometerObservationCapability.java b/micrometer/src/main/java/feign/micrometer/MicrometerObservationCapability.java index e49f43d40e..dfa5eaabd3 100644 --- a/micrometer/src/main/java/feign/micrometer/MicrometerObservationCapability.java +++ b/micrometer/src/main/java/feign/micrometer/MicrometerObservationCapability.java @@ -18,10 +18,10 @@ import feign.AsyncClient; import feign.Capability; import feign.Client; -import feign.FeignException; import feign.Response; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; +import java.util.concurrent.CompletionException; /** Wrap feign {@link Client} with metrics. */ public class MicrometerObservationCapability implements Capability { @@ -55,13 +55,15 @@ public Client enrich(Client client) { this.observationRegistry) .start(); - try { + try (Observation.Scope scope = observation.openScope()) { Response response = client.execute(request, options); - finalizeObservation(feignContext, observation, null, response); + feignContext.setResponse(response); return response; - } catch (FeignException ex) { - finalizeObservation(feignContext, observation, ex, null); + } catch (Throwable ex) { + observation.error(ex); throw ex; + } finally { + observation.stop(); } }; } @@ -80,24 +82,29 @@ public AsyncClient enrich(AsyncClient client) { this.observationRegistry) .start(); - try { + try (Observation.Scope scope = observation.openScope()) { return client .execute(feignContext.getCarrier(), options, context) - .whenComplete((r, ex) -> finalizeObservation(feignContext, observation, ex, r)); - } catch (FeignException ex) { - finalizeObservation(feignContext, observation, ex, null); - + .whenComplete( + (response, ex) -> { + feignContext.setResponse(response); + if (ex != null) { + observation.error(unwrap(ex)); + } + observation.stop(); + }); + } catch (Throwable ex) { + observation.error(ex); + observation.stop(); throw ex; } }; } - private void finalizeObservation( - FeignContext feignContext, Observation observation, Throwable ex, Response response) { - feignContext.setResponse(response); - if (ex != null) { - observation.error(ex); + private static Throwable unwrap(Throwable ex) { + if (ex instanceof CompletionException && ex.getCause() != null) { + return ex.getCause(); } - observation.stop(); + return ex; } } diff --git a/micrometer/src/test/java/feign/micrometer/MicrometerObservationCapabilityTest.java b/micrometer/src/test/java/feign/micrometer/MicrometerObservationCapabilityTest.java new file mode 100644 index 0000000000..b2f75d09ef --- /dev/null +++ b/micrometer/src/test/java/feign/micrometer/MicrometerObservationCapabilityTest.java @@ -0,0 +1,302 @@ +/* + * 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.micrometer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import feign.AsyncClient; +import feign.AsyncFeign; +import feign.Client; +import feign.Feign; +import feign.RequestLine; +import feign.Retryer; +import feign.Target.HardCodedTarget; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.tck.TestObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistryAssert; +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Behavioural tests for {@link MicrometerObservationCapability} focusing on the cases that the + * existing capability did not cover: + * + *
    + *
  • The observation is current (via {@code Observation.Scope}) while the client executes, so + * downstream handlers — most notably tracing handlers that propagate trace/span ids into the + * MDC — see the Feign-scoped observation rather than the parent. + *
  • Exceptions thrown by the underlying client that are not {@code FeignException} — + * {@code IOException}, {@code SocketTimeoutException}, runtime exceptions thrown by a custom + * {@code ErrorDecoder}, and so on — still close the observation and record the error. + *
+ */ +class MicrometerObservationCapabilityTest { + + interface TestClient { + @RequestLine("GET /") + String get(); + } + + interface AsyncTestClient { + @RequestLine("GET /") + CompletableFuture get(); + } + + private TestObservationRegistry observationRegistry; + + @BeforeEach + void setUp() { + this.observationRegistry = TestObservationRegistry.create(); + } + + @Test + void scopeIsOpenDuringClientExecution() { + AtomicReference observedDuringExecute = new AtomicReference<>(); + Client client = + (request, options) -> { + observedDuringExecute.set(observationRegistry.getCurrentObservation()); + return feign.Response.builder() + .status(200) + .reason("OK") + .request(request) + .headers(java.util.Collections.emptyMap()) + .build(); + }; + + TestClient feignClient = + Feign.builder() + .client(client) + .addCapability(new MicrometerObservationCapability(observationRegistry)) + .target(new HardCodedTarget<>(TestClient.class, "http://localhost")); + + feignClient.get(); + + assertThat(observedDuringExecute.get()) + .as("observation must be active (scoped) while the underlying client executes") + .isNotNull(); + assertThat(observationRegistry.getCurrentObservation()) + .as("observation must no longer be current after the call returns") + .isNull(); + } + + @Test + void recordsNonFeignExceptionThrownByClient() { + // Feign's SynchronousMethodHandler wraps IOExceptions, so callers don't see the original + // throwable. The observation captured by the capability sits below that wrapping, and is the + // only place the underlying error is recorded against the trace. + SocketTimeoutException underlying = new SocketTimeoutException("connect timed out"); + + Client client = + (request, options) -> { + throw underlying; + }; + + TestClient feignClient = + Feign.builder() + .client(client) + .retryer(Retryer.NEVER_RETRY) + .addCapability(new MicrometerObservationCapability(observationRegistry)) + .target(new HardCodedTarget<>(TestClient.class, "http://localhost")); + + assertThatThrownBy(feignClient::get).isInstanceOf(Exception.class); + + TestObservationRegistryAssert.assertThat(observationRegistry) + .hasSingleObservationThat() + .hasBeenStopped() + .hasError(underlying); + } + + @Test + void recordsRuntimeExceptionThrownByClient() { + RuntimeException underlying = new RuntimeException("boom"); + + Client client = + (request, options) -> { + throw underlying; + }; + + TestClient feignClient = + Feign.builder() + .client(client) + .retryer(Retryer.NEVER_RETRY) + .addCapability(new MicrometerObservationCapability(observationRegistry)) + .target(new HardCodedTarget<>(TestClient.class, "http://localhost")); + + assertThatThrownBy(feignClient::get).isInstanceOf(Exception.class); + + TestObservationRegistryAssert.assertThat(observationRegistry) + .hasSingleObservationThat() + .hasBeenStopped() + .hasError(underlying); + } + + @Test + void asyncScopeIsOpenDuringClientExecution() { + AtomicReference observedDuringExecute = new AtomicReference<>(); + AsyncClient client = + (request, options, context) -> { + observedDuringExecute.set(observationRegistry.getCurrentObservation()); + return CompletableFuture.completedFuture( + feign.Response.builder() + .status(200) + .reason("OK") + .request(request) + .headers(java.util.Collections.emptyMap()) + .build()); + }; + + AsyncTestClient feignClient = + AsyncFeign.builder() + .client(client) + .addCapability(new MicrometerObservationCapability(observationRegistry)) + .target(new HardCodedTarget<>(AsyncTestClient.class, "http://localhost")); + + feignClient.get().join(); + + assertThat(observedDuringExecute.get()) + .as("observation must be active while the async client kicks off the request") + .isNotNull(); + } + + @Test + void asyncRecordsNonFeignExceptionFromFailedFuture() { + IOException underlying = new IOException("connection reset"); + + AsyncClient client = + (request, options, context) -> { + CompletableFuture failed = new CompletableFuture<>(); + failed.completeExceptionally(underlying); + return failed; + }; + + AsyncTestClient feignClient = + AsyncFeign.builder() + .client(client) + .retryer(Retryer.NEVER_RETRY) + .addCapability(new MicrometerObservationCapability(observationRegistry)) + .target(new HardCodedTarget<>(AsyncTestClient.class, "http://localhost")); + + assertThatThrownBy(() -> feignClient.get().join()).isInstanceOf(CompletionException.class); + + TestObservationRegistryAssert.assertThat(observationRegistry) + .hasSingleObservationThat() + .hasBeenStopped() + .hasError(underlying); + } + + @Test + void asyncRecordsSynchronousExceptionFromClient() { + RuntimeException underlying = new RuntimeException("immediate failure"); + + AsyncClient client = + (request, options, context) -> { + throw underlying; + }; + + AsyncTestClient feignClient = + AsyncFeign.builder() + .client(client) + .retryer(Retryer.NEVER_RETRY) + .addCapability(new MicrometerObservationCapability(observationRegistry)) + .target(new HardCodedTarget<>(AsyncTestClient.class, "http://localhost")); + + assertThatThrownBy(feignClient::get).isInstanceOf(RuntimeException.class); + + TestObservationRegistryAssert.assertThat(observationRegistry) + .hasSingleObservationThat() + .hasBeenStopped() + .hasError(underlying); + } + + @Test + void parentObservationIsRestoredAfterCall() { + Observation parent = Observation.start("parent", observationRegistry); + try (Observation.Scope ignored = parent.openScope()) { + + Client client = + (request, options) -> + feign.Response.builder() + .status(200) + .reason("OK") + .request(request) + .headers(java.util.Collections.emptyMap()) + .build(); + + TestClient feignClient = + Feign.builder() + .client(client) + .addCapability(new MicrometerObservationCapability(observationRegistry)) + .target(new HardCodedTarget<>(TestClient.class, "http://localhost")); + + feignClient.get(); + + assertThat(observationRegistry.getCurrentObservation()) + .as("parent observation must still be current after the Feign call completes") + .isSameAs(parent); + } finally { + parent.stop(); + } + } + + @Test + @SuppressWarnings("unchecked") + void scopeReceivesObservationCarryingFeignContext() { + AtomicReference seenContext = new AtomicReference<>(); + + observationRegistry + .observationConfig() + .observationHandler( + new ObservationHandler() { + @Override + public void onScopeOpened(Observation.Context context) { + seenContext.set(context); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return true; + } + }); + + Client client = + (request, options) -> + feign.Response.builder() + .status(200) + .reason("OK") + .request(request) + .headers(java.util.Collections.emptyMap()) + .build(); + + TestClient feignClient = + Feign.builder() + .client(client) + .addCapability(new MicrometerObservationCapability(observationRegistry)) + .target(new HardCodedTarget<>(TestClient.class, "http://localhost")); + + feignClient.get(); + + assertThat(seenContext.get()) + .as("scope must propagate the FeignContext to ObservationHandler#onScopeOpened") + .isInstanceOf(FeignContext.class); + } +} From e6e3a7beca9c84fbcf23f7fec67b298806c34017 Mon Sep 17 00:00:00 2001 From: seonwoojung Date: Thu, 11 Jun 2026 20:17:26 +0900 Subject: [PATCH 03/51] Avoid ClassLoader leak from static default async executor (#3178) (#3394) * Avoid ClassLoader leak from static default async executor AsyncFeign held its default ExecutorService in a static singleton (LazyInitializedExecutorService) that was never shut down. Inside a servlet container, the daemon thread pool retained a strong reference to the initial application's ContextClassLoader, so each redeployment leaked the previous application's ClassLoader and threads, eventually causing a Metaspace OutOfMemoryError. Replace the static singleton with an instance-scoped default executor created per built client, so it (and its threads) become eligible for GC once the client is discarded. Also add AsyncBuilder.executorService(...) so callers can supply and own a managed, shut-downable executor. Fixes #3178 Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> * Exclude async executor from capability enrichment --------- Signed-off-by: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) --- core/src/main/java/feign/AsyncFeign.java | 52 +++++++++++++++----- core/src/main/java/feign/BaseBuilder.java | 2 + core/src/test/java/feign/AsyncFeignTest.java | 37 ++++++++++++++ 3 files changed, 79 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/feign/AsyncFeign.java b/core/src/main/java/feign/AsyncFeign.java index 08cfcc96c6..38830b4234 100644 --- a/core/src/main/java/feign/AsyncFeign.java +++ b/core/src/main/java/feign/AsyncFeign.java @@ -56,23 +56,28 @@ public static AsyncBuilder asyncBuilder() { return builder(); } - private static class LazyInitializedExecutorService { - - private static final ExecutorService instance = - Executors.newCachedThreadPool( - r -> { - final Thread result = new Thread(r); - result.setDaemon(true); - return result; - }); + /** + * Creates the default {@link ExecutorService} used when neither a custom {@link AsyncClient} nor + * a custom executor is provided. The executor is created per built client (instance scoped) + * rather than being held in a static singleton, so that it - together with its daemon threads - + * becomes eligible for garbage collection once the owning client is discarded. This avoids the + * {@code ClassLoader} leak that a static singleton causes on application redeployments inside + * servlet containers (see gh-3178). + */ + private static ExecutorService defaultExecutorService() { + return Executors.newCachedThreadPool( + r -> { + final Thread result = new Thread(r); + result.setDaemon(true); + return result; + }); } public static class AsyncBuilder extends BaseBuilder, AsyncFeign> { private AsyncContextSupplier defaultContextSupplier = () -> null; - private AsyncClient client = - new DefaultAsyncClient<>( - new DefaultClient(null, null), LazyInitializedExecutorService.instance); + private AsyncClient client; + private ExecutorService executorService; private MethodInfoResolver methodInfoResolver = MethodInfo::new; @Deprecated @@ -86,6 +91,28 @@ public AsyncBuilder client(AsyncClient client) { return this; } + /** + * Sets the {@link ExecutorService} used by the default {@link AsyncClient} to run the + * (blocking) underlying client calls. When provided, the caller owns the executor's lifecycle + * and is responsible for shutting it down. This is the recommended way to avoid the {@code + * ClassLoader} leak described in gh-3178: supply a managed, shut-downable executor instead of + * relying on the built-in default. Ignored when a custom {@link #client(AsyncClient)} is + * supplied. + */ + public AsyncBuilder executorService(ExecutorService executorService) { + this.executorService = executorService; + return this; + } + + private AsyncClient resolveClient() { + if (client != null) { + return client; + } + final ExecutorService executor = + executorService != null ? executorService : defaultExecutorService(); + return new DefaultAsyncClient<>(new DefaultClient(null, null), executor); + } + public AsyncBuilder methodInfoResolver(MethodInfoResolver methodInfoResolver) { this.methodInfoResolver = methodInfoResolver; return this; @@ -206,6 +233,7 @@ public AsyncBuilder invocationHandlerFactory( @Override public AsyncFeign internalBuild() { + final AsyncClient client = resolveClient(); AsyncResponseHandler responseHandler = (AsyncResponseHandler) Capability.enrich( diff --git a/core/src/main/java/feign/BaseBuilder.java b/core/src/main/java/feign/BaseBuilder.java index 05608dc2ed..d279ed764a 100644 --- a/core/src/main/java/feign/BaseBuilder.java +++ b/core/src/main/java/feign/BaseBuilder.java @@ -362,6 +362,8 @@ List getFieldsToEnrich() { .filter(field -> !Objects.equals(field.getName(), "requestInterceptors")) .filter(field -> !Objects.equals(field.getName(), "responseInterceptors")) .filter(field -> !Objects.equals(field.getName(), "methodInterceptors")) + // caller-owned lifecycle resources are not capability-enriched + .filter(field -> !Objects.equals(field.getName(), "executorService")) // skip primitive types .filter(field -> !field.getType().isPrimitive()) // skip enumerations diff --git a/core/src/test/java/feign/AsyncFeignTest.java b/core/src/test/java/feign/AsyncFeignTest.java index 08c70f19c4..2e683de5f8 100644 --- a/core/src/test/java/feign/AsyncFeignTest.java +++ b/core/src/test/java/feign/AsyncFeignTest.java @@ -23,6 +23,10 @@ import static org.assertj.core.data.MapEntry.entry; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; @@ -103,6 +107,39 @@ void postTemplateParamsResolve() throws Exception { + " \"password\"}"); } + @Test + void usesProvidedExecutorService() throws Exception { + server.enqueue(new MockResponse().setBody("foo")); + + ExecutorService executorService = spy(Executors.newCachedThreadPool()); + try { + TestInterfaceAsync api = + AsyncFeign.builder() + .decoder(new DefaultDecoder()) + .executorService(executorService) + .target(TestInterfaceAsync.class, "http://localhost:" + server.getPort()); + + String result = api.post().join(); + + assertThat(result).isEqualTo("foo"); + verify(executorService, atLeastOnce()).submit(any(Runnable.class)); + } finally { + executorService.shutdownNow(); + } + } + + @Test + void defaultExecutorServiceStillExecutesRequests() throws Exception { + server.enqueue(new MockResponse().setBody("foo")); + + TestInterfaceAsync api = + AsyncFeign.builder() + .decoder(new DefaultDecoder()) + .target(TestInterfaceAsync.class, "http://localhost:" + server.getPort()); + + assertThat(api.post().join()).isEqualTo("foo"); + } + @Test void postFormParams() throws Exception { server.enqueue(new MockResponse().setBody("foo")); From 18fe77168bc4a3c7309e631b565f1cc455728b4a Mon Sep 17 00:00:00 2001 From: seonwoojung Date: Thu, 11 Jun 2026 21:43:58 +0900 Subject: [PATCH 04/51] Fix feign-core test compilation broken by removed Request.create overload (#3406) * fix: adapt logger tests to the Request.create(Body, RequestTemplate) signature JavaLoggerTest and LoggerRebufferTest still called the Request.create(..., byte[], Charset) overload removed in f9159cfa, breaking test compilation of feign-core on master. Use the (Body, RequestTemplate) signature with null arguments, matching the existing usage in RetryableExceptionTest. * fix: adapt Slf4jLoggerTest to the Request.create(Body, RequestTemplate) signature Slf4jLoggerTest had the same problem as the feign-core logger tests: it called the removed Request.create(..., byte[], Charset) overload and also referenced feign.Util without an import, breaking feign-slf4j test compilation once feign-core compiles again. * fix: adapt validation tests to the RequestTemplate#body(Request.Body) signature Both BeanValidationMethodInterceptorTest variants still used the RequestTemplate#body(String) overload removed in f9159cfa. * test: make Http2ClientTest hermetic, dropping the live nghttp2.org dependency that hangs CI Signed-off-by: Marvin Froeder --------- Signed-off-by: Marvin Froeder Co-authored-by: Marvin Froeder Co-authored-by: Marvin --- core/src/test/java/feign/JavaLoggerTest.java | 9 +- .../test/java/feign/LoggerRebufferTest.java | 19 ++-- .../http2client/test/Http2ClientTest.java | 100 +++++++++++++++--- .../java/feign/slf4j/Slf4jLoggerTest.java | 10 +- .../BeanValidationMethodInterceptorTest.java | 4 +- .../BeanValidationMethodInterceptorTest.java | 4 +- 6 files changed, 103 insertions(+), 43 deletions(-) diff --git a/core/src/test/java/feign/JavaLoggerTest.java b/core/src/test/java/feign/JavaLoggerTest.java index f05d839e15..3799d79b4b 100644 --- a/core/src/test/java/feign/JavaLoggerTest.java +++ b/core/src/test/java/feign/JavaLoggerTest.java @@ -43,8 +43,7 @@ void rebuffersResponseBodyWhenJulLevelIsInfo() 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("{\"error\":\"test\"}", Util.UTF_8) .build(); @@ -69,8 +68,7 @@ void rebuffersResponseBodyWhenJulLevelIsWarning() throws Exception { 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("{\"message\":\"error details\"}", Util.UTF_8) .build(); @@ -95,8 +93,7 @@ void responseBodyReadableMultipleTimesForErrorDecoder() throws Exception { .status(400) .reason("Bad Request") .request( - Request.create( - HttpMethod.POST, "/api/submit", Collections.emptyMap(), null, Util.UTF_8)) + Request.create(HttpMethod.POST, "/api/submit", Collections.emptyMap(), null, null)) .headers(Collections.>emptyMap()) .body(originalBody, Util.UTF_8) .build(); diff --git a/core/src/test/java/feign/LoggerRebufferTest.java b/core/src/test/java/feign/LoggerRebufferTest.java index 5928a8f255..ab1e1f2309 100644 --- a/core/src/test/java/feign/LoggerRebufferTest.java +++ b/core/src/test/java/feign/LoggerRebufferTest.java @@ -43,8 +43,7 @@ void headersLevelRebuffersResponseBody() throws Exception { .status(404) .reason("Not Found") .request( - Request.create( - HttpMethod.GET, "/api/resource", Collections.emptyMap(), null, Util.UTF_8)) + Request.create(HttpMethod.GET, "/api/resource", Collections.emptyMap(), null, null)) .headers(Collections.>emptyMap()) .body(originalBody, Util.UTF_8) .build(); @@ -70,8 +69,7 @@ void basicLevelDoesNotRebufferResponseBody() throws Exception { .status(200) .reason("OK") .request( - Request.create( - HttpMethod.GET, "/api/resource", Collections.emptyMap(), null, Util.UTF_8)) + Request.create(HttpMethod.GET, "/api/resource", Collections.emptyMap(), null, null)) .headers(Collections.>emptyMap()) .body(originalBody, Util.UTF_8) .build(); @@ -94,8 +92,7 @@ void fullLevelRebuffersResponseBody() throws Exception { .status(200) .reason("OK") .request( - Request.create( - HttpMethod.POST, "/api/create", Collections.emptyMap(), null, Util.UTF_8)) + Request.create(HttpMethod.POST, "/api/create", Collections.emptyMap(), null, null)) .headers(Collections.>emptyMap()) .body(originalBody, Util.UTF_8) .build(); @@ -122,8 +119,7 @@ void noneLevelDoesNotRebufferResponseBody() throws Exception { .status(200) .reason("OK") .request( - Request.create( - HttpMethod.GET, "/api/status", Collections.emptyMap(), null, Util.UTF_8)) + Request.create(HttpMethod.GET, "/api/status", Collections.emptyMap(), null, null)) .headers(Collections.>emptyMap()) .body(originalBody, Util.UTF_8) .build(); @@ -146,7 +142,7 @@ void http204DoesNotRebufferEvenAtHeadersLevel() throws Exception { .reason("No Content") .request( Request.create( - HttpMethod.DELETE, "/api/resource/1", Collections.emptyMap(), null, Util.UTF_8)) + HttpMethod.DELETE, "/api/resource/1", Collections.emptyMap(), null, null)) .headers(Collections.>emptyMap()) .body("should be ignored", Util.UTF_8) .build(); @@ -168,8 +164,7 @@ void http205DoesNotRebufferEvenAtHeadersLevel() throws Exception { .status(205) .reason("Reset Content") .request( - Request.create( - HttpMethod.POST, "/api/form", Collections.emptyMap(), null, Util.UTF_8)) + Request.create(HttpMethod.POST, "/api/form", Collections.emptyMap(), null, null)) .headers(Collections.>emptyMap()) .body("should be ignored", Util.UTF_8) .build(); @@ -192,7 +187,7 @@ void nullBodyHandledCorrectlyAtHeadersLevel() throws Exception { .reason("OK") .request( Request.create( - HttpMethod.HEAD, "/api/resource", Collections.emptyMap(), null, Util.UTF_8)) + HttpMethod.HEAD, "/api/resource", Collections.emptyMap(), null, null)) .headers(Collections.>emptyMap()) .body((byte[]) null) .build(); diff --git a/java11/src/test/java/feign/http2client/test/Http2ClientTest.java b/java11/src/test/java/feign/http2client/test/Http2ClientTest.java index a769bda6b2..4f3602322b 100644 --- a/java11/src/test/java/feign/http2client/test/Http2ClientTest.java +++ b/java11/src/test/java/feign/http2client/test/Http2ClientTest.java @@ -26,16 +26,20 @@ import feign.http2client.Http2Client; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; import java.net.http.HttpTimeoutException; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.RecordedRequest; -import org.json.JSONException; import org.junit.jupiter.api.Test; -import org.skyscreamer.jsonassert.JSONAssert; /** Tests client-specific behavior, such as ensuring Content-Length is sent when specified. */ public class Http2ClientTest extends AbstractClientTest { @@ -83,17 +87,39 @@ public interface TestInterface { @Override @Test public void patch() throws Exception { + server.enqueue(new MockResponse().setBody("foo")); + final TestInterface api = - newBuilder().target(TestInterface.class, "https://nghttp2.org/httpbin/"); - assertThat(api.patch("")).contains("https://nghttp2.org/httpbin/patch"); + newBuilder().target(TestInterface.class, "http://localhost:" + server.getPort()); + + assertThat(api.patch("some text")).isEqualTo("foo"); + + MockWebServerAssertions.assertThat(server.takeRequest()) + .hasMethod("PATCH") + .hasPath("/patch") + .hasBody("some text"); } @Override @Test public void noResponseBodyForPatch() { + server.enqueue(new MockResponse()); + final TestInterface api = - newBuilder().target(TestInterface.class, "https://nghttp2.org/httpbin/"); - assertThat(api.patch()).contains("https://nghttp2.org/httpbin/patch"); + newBuilder().target(TestInterface.class, "http://localhost:" + server.getPort()); + + assertThat(api.patch()).isEmpty(); + + MockWebServerAssertions.assertThat(takeRequest()).hasMethod("PATCH").hasPath("/patch"); + } + + private RecordedRequest takeRequest() { + try { + return server.takeRequest(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } } @Override @@ -150,21 +176,61 @@ void timeoutTest() { } @Test - void getWithRequestBody() throws JSONException { - final TestInterface api = - newBuilder().target(TestInterface.class, "https://nghttp2.org/httpbin/"); - String result = api.getWithBody(); - String expected = "{ \"data\":\"some request body\" }"; - JSONAssert.assertEquals(expected, result, false); + void getWithRequestBody() throws Exception { + // MockWebServer rejects GET requests carrying a body ("Request must not have a body"), + // so this test runs against a minimal local socket server instead. + try (ServerSocket httpServer = new ServerSocket(0, 1, InetAddress.getLoopbackAddress())) { + final AtomicReference receivedRequest = new AtomicReference<>(); + final Thread serverThread = + new Thread( + () -> { + try (Socket socket = httpServer.accept()) { + socket.setSoTimeout(5000); + final InputStream in = socket.getInputStream(); + final StringBuilder request = new StringBuilder(); + final byte[] buffer = new byte[8192]; + while (!request.toString().contains("some request body")) { + final int read = in.read(buffer); + if (read == -1) { + break; + } + request.append(new String(buffer, 0, read, UTF_8)); + } + receivedRequest.set(request.toString()); + socket + .getOutputStream() + .write("HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nfoo".getBytes(UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + serverThread.start(); + + final TestInterface api = + newBuilder().target(TestInterface.class, "http://localhost:" + httpServer.getLocalPort()); + + assertThat(api.getWithBody()).isEqualTo("foo"); + + serverThread.join(5000); + assertThat(receivedRequest.get()) + .startsWith("GET /anything HTTP/1.1") + .contains("some request body"); + } } @Test - void deleteWithRequestBody() throws JSONException { + void deleteWithRequestBody() throws Exception { + server.enqueue(new MockResponse().setBody("foo")); + final TestInterface api = - newBuilder().target(TestInterface.class, "https://nghttp2.org/httpbin/"); - String result = api.deleteWithBody(); - String expected = "{ \"data\":\"some request body\" }"; - JSONAssert.assertEquals(expected, result, false); + newBuilder().target(TestInterface.class, "http://localhost:" + server.getPort()); + + assertThat(api.deleteWithBody()).isEqualTo("foo"); + + MockWebServerAssertions.assertThat(server.takeRequest()) + .hasMethod("DELETE") + .hasPath("/anything") + .hasBody("some request body"); } @Override diff --git a/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java b/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java index 12973ff4e9..6df54f9ef7 100644 --- a/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java +++ b/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java @@ -21,6 +21,7 @@ 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; @@ -124,8 +125,7 @@ void rebuffersResponseBodyWhenLogLevelIsInfo() 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("{\"error\":\"test\"}", Util.UTF_8) .build(); @@ -150,8 +150,7 @@ void rebuffersResponseBodyWhenLogLevelIsFull() throws Exception { 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("{\"message\":\"error details\"}", Util.UTF_8) .build(); @@ -177,8 +176,7 @@ void responseBodyReadableMultipleTimes() throws Exception { .status(400) .reason("Bad Request") .request( - Request.create( - HttpMethod.POST, "/api/submit", Collections.emptyMap(), null, Util.UTF_8)) + Request.create(HttpMethod.POST, "/api/submit", Collections.emptyMap(), null, null)) .headers(Collections.>emptyMap()) .body(originalBody, Util.UTF_8) .build(); diff --git a/validation-jakarta/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java b/validation-jakarta/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java index b2c977a242..36748ac295 100644 --- a/validation-jakarta/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java +++ b/validation-jakarta/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java @@ -20,6 +20,7 @@ import feign.Feign; import feign.Param; +import feign.Request; import feign.RequestLine; import jakarta.validation.ConstraintViolationException; import jakarta.validation.Valid; @@ -76,7 +77,8 @@ interface Api { private Api api() { return Feign.builder() - .encoder((object, bodyType, template) -> template.body(String.valueOf(object))) + .encoder( + (object, bodyType, template) -> template.body(Request.Body.of(String.valueOf(object)))) .methodInterceptor(BeanValidationMethodInterceptor.usingDefaultFactory()) .target(Api.class, "http://localhost:" + server.getPort()); } diff --git a/validation/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java b/validation/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java index 11c19dd111..a25b393a38 100644 --- a/validation/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java +++ b/validation/src/test/java/feign/validation/BeanValidationMethodInterceptorTest.java @@ -20,6 +20,7 @@ import feign.Feign; import feign.Param; +import feign.Request; import feign.RequestLine; import javax.validation.ConstraintViolationException; import javax.validation.Valid; @@ -76,7 +77,8 @@ interface Api { private Api api() { return Feign.builder() - .encoder((object, bodyType, template) -> template.body(String.valueOf(object))) + .encoder( + (object, bodyType, template) -> template.body(Request.Body.of(String.valueOf(object)))) .methodInterceptor(BeanValidationMethodInterceptor.usingDefaultFactory()) .target(Api.class, "http://localhost:" + server.getPort()); } From 216dbf7349dd3544cb794188314a6cbbaa85b034 Mon Sep 17 00:00:00 2001 From: Marvin Date: Thu, 11 Jun 2026 11:44:14 -0300 Subject: [PATCH 05/51] revert: move Request.Body streaming breaking changes off master, they belong to 14.x (#3409) Signed-off-by: Marvin Froeder --- MIGRATION-v14.md | 421 ------------- .../java/feign/error/ExceptionGenerator.java | 4 +- .../AbstractAnnotationErrorDecoderTest.java | 4 +- ...ErrorDecoderExceptionConstructorsTest.java | 7 +- .../benchmark/DecoderIteratorsBenchmark.java | 2 +- core/src/main/java/feign/DefaultClient.java | 9 +- core/src/main/java/feign/DefaultContract.java | 2 +- core/src/main/java/feign/FeignException.java | 6 +- core/src/main/java/feign/Logger.java | 22 +- core/src/main/java/feign/Request.java | 573 +++++++++--------- core/src/main/java/feign/RequestTemplate.java | 60 +- .../main/java/feign/codec/DefaultEncoder.java | 5 +- .../feign/AlwaysEncodeBodyContractTest.java | 15 +- core/src/test/java/feign/AsyncFeignTest.java | 11 +- core/src/test/java/feign/ClientTest.java | 6 +- .../test/java/feign/DefaultContractTest.java | 31 +- .../src/test/java/feign/FeignBuilderTest.java | 3 +- .../test/java/feign/FeignExceptionTest.java | 39 +- core/src/test/java/feign/FeignTest.java | 11 +- .../test/java/feign/FeignUnderAsyncTest.java | 11 +- core/src/test/java/feign/JavaLoggerTest.java | 9 +- .../test/java/feign/LoggerMethodsTest.java | 2 +- .../test/java/feign/LoggerRebufferTest.java | 19 +- .../test/java/feign/RequestTemplateTest.java | 13 +- core/src/test/java/feign/ResponseTest.java | 23 +- .../java/feign/RetryableExceptionTest.java | 6 +- core/src/test/java/feign/RetryerTest.java | 2 +- core/src/test/java/feign/TargetTest.java | 4 +- .../feign/assertj/RequestTemplateAssert.java | 26 +- .../java/feign/codec/DefaultDecoderTest.java | 5 +- .../java/feign/codec/DefaultEncoderTest.java | 17 +- .../DefaultErrorDecoderHttpErrorTest.java | 6 +- .../feign/codec/DefaultErrorDecoderTest.java | 19 +- .../java/feign/stream/StreamDecoderTest.java | 4 +- .../java/feign/metrics4/MeteredEncoder.java | 18 +- .../java/feign/metrics5/MeteredEncoder.java | 18 +- .../feign/fastjson2/Fastjson2Encoder.java | 4 +- .../feign/fastjson2/FastJsonCodecTest.java | 16 +- .../form/MultipartFormContentProcessor.java | 3 +- .../form/UrlencodedFormContentProcessor.java | 3 +- .../feign/form/multipart/DelegateWriter.java | 16 +- .../googlehttpclient/GoogleHttpClient.java | 60 +- .../java/feign/graphql/GraphqlDecoder.java | 11 +- .../graphql/GraphqlRequestInterceptor.java | 2 +- .../feign/graphql/GraphqlDecoderTest.java | 6 +- .../feign/graphql/GraphqlEncoderTest.java | 33 +- .../src/main/java/feign/gson/GsonEncoder.java | 3 +- .../test/java/feign/gson/GsonCodecTest.java | 19 +- .../java/feign/hc5/ApacheHttp5Client.java | 22 +- .../feign/hc5/AsyncApacheHttp5Client.java | 125 +--- .../main/java/feign/hc5/FeignBodyEntity.java | 97 --- .../feign/hc5/AsyncApacheHttp5ClientTest.java | 11 +- .../feign/httpclient/ApacheHttpClient.java | 64 +- .../jackson/jaxb/JacksonJaxbJsonEncoder.java | 4 +- .../jackson/jaxb/JacksonJaxbCodecTest.java | 7 +- .../feign/jackson/jr/JacksonJrEncoder.java | 5 +- .../feign/jackson/jr/JacksonCodecTest.java | 32 +- .../java/feign/jackson/JacksonEncoder.java | 4 +- .../java/feign/jackson/JacksonCodecTest.java | 31 +- .../feign/jackson/JacksonIteratorTest.java | 10 +- .../java/feign/jackson3/Jackson3Encoder.java | 4 +- .../feign/jackson3/Jackson3CodecTest.java | 31 +- .../java/feign/http2client/Http2Client.java | 110 +--- .../test/Http2ClientAsyncTest.java | 11 +- .../src/main/java/feign/jaxb/JAXBEncoder.java | 3 +- .../test/java/feign/jaxb/JAXBCodecTest.java | 27 +- .../jaxb/examples/AWSSignatureVersion4.java | 11 +- .../src/main/java/feign/jaxb/JAXBEncoder.java | 3 +- .../test/java/feign/jaxb/JAXBCodecTest.java | 27 +- .../jaxb/examples/AWSSignatureVersion4.java | 11 +- .../main/java/feign/jaxrs2/JAXRSClient.java | 29 +- .../src/main/java/feign/json/JsonEncoder.java | 3 +- .../test/java/feign/json/JsonCodecTest.java | 7 +- .../test/java/feign/json/JsonDecoderTest.java | 4 +- .../test/java/feign/json/JsonEncoderTest.java | 18 +- .../kotlin/feign/kotlin/CoroutineFeignTest.kt | 5 +- .../java/feign/micrometer/MeteredEncoder.java | 16 +- mock/src/main/java/feign/mock/RequestKey.java | 47 +- .../test/java/feign/mock/MockClientTest.java | 41 +- .../test/java/feign/mock/RequestKeyTest.java | 35 +- .../main/java/feign/moshi/MoshiEncoder.java | 3 +- .../java/feign/moshi/MoshiDecoderTest.java | 19 +- .../main/java/feign/okhttp/OkHttpClient.java | 56 +- .../feign/okhttp/OkHttpClientAsyncTest.java | 11 +- pom.xml | 8 - .../src/main/java/feign/ribbon/LBClient.java | 13 +- .../test/java/feign/ribbon/LBClientTest.java | 4 +- .../test/java/feign/sax/SAXDecoderTest.java | 9 +- .../sax/examples/AWSSignatureVersion4.java | 11 +- .../java/feign/slf4j/Slf4jLoggerTest.java | 11 +- .../src/main/java/feign/soap/SOAPEncoder.java | 9 +- .../test/java/feign/soap/SOAPCodecTest.java | 27 +- .../java/feign/soap/SOAPFaultDecoderTest.java | 13 +- .../src/main/java/feign/soap/SOAPEncoder.java | 3 +- .../test/java/feign/soap/SOAPCodecTest.java | 27 +- .../java/feign/soap/SOAPFaultDecoderTest.java | 13 +- .../java/feign/spring/SpringContractTest.java | 11 +- .../BeanValidationMethodInterceptorTest.java | 4 +- .../BeanValidationMethodInterceptorTest.java | 4 +- .../src/main/java/feign/VertxFeign.java | 11 +- .../java/feign/vertx/OutputToReadStream.java | 308 ---------- .../java/feign/vertx/VertxHttpClient.java | 26 +- .../feign/vertx/ConnectionsLeakTests.java | 2 - .../vertx/Http11ClientReconnectTest.java | 1 - .../java/feign/vertx/QueryMapEncoderTest.java | 1 - .../java/feign/vertx/RawContractTest.java | 1 - .../feign/vertx/RequestPreProcessorTest.java | 1 - .../test/java/feign/vertx/RetryingTest.java | 1 - .../java/feign/vertx/TimeoutHandlingTest.java | 1 - .../java/feign/vertx/VertxHttpClientTest.java | 20 - .../feign/vertx/VertxHttpOptionsTest.java | 2 - .../feign/vertx/ConnectionsLeakTests.java | 2 - .../vertx/Http11ClientReconnectTest.java | 1 - .../java/feign/vertx/QueryMapEncoderTest.java | 1 - .../java/feign/vertx/RawContractTest.java | 1 - .../feign/vertx/RequestPreProcessorTest.java | 1 - .../test/java/feign/vertx/RetryingTest.java | 1 - .../java/feign/vertx/TimeoutHandlingTest.java | 1 - .../java/feign/vertx/VertxHttpClientTest.java | 20 - .../feign/vertx/VertxHttpOptionsTest.java | 2 - 120 files changed, 1037 insertions(+), 2088 deletions(-) delete mode 100644 MIGRATION-v14.md delete mode 100644 hc5/src/main/java/feign/hc5/FeignBodyEntity.java delete mode 100644 vertx/feign-vertx/src/main/java/feign/vertx/OutputToReadStream.java diff --git a/MIGRATION-v14.md b/MIGRATION-v14.md deleted file mode 100644 index e489aa707a..0000000000 --- a/MIGRATION-v14.md +++ /dev/null @@ -1,421 +0,0 @@ -# Migration Guide — Feign v14 (Request Body Streaming) - -This guide covers the breaking changes introduced in #3360 and explains how to update your code. - -> **Target release:** `v14.rc.1` - ---- - -## Overview - -`feign.Request.Body` has been redesigned from a `byte[]`-backed concrete class into a **streaming-ready interface**. -Request bodies are no longer eagerly buffered in memory unless you explicitly use the `byte[]`/`String` factory methods. - -For most users, regular Feign usage via interface annotations and `Feign.builder().target(...)` is unchanged. -The **breaking changes** primarily affect code that interacts directly with request body internals, including: - -- Custom `Encoder` implementations -- Custom `Client` implementations -- Any code that directly reads `Request.body()`, `Request.length()`, `Request.charset()`, or `RequestTemplate.body()`/ - `RequestTemplate.requestBody()` - ---- - -## Breaking Changes - -### 1. `Request.Body` is now an interface - -**Before:** - -```java -// Body was a concrete class with public fields/methods -Request.Body body = Request.Body.create("hello", StandardCharsets.UTF_8); -byte[] bytes = body.asBytes(); -String str = body.asString(); -int len = body.length(); -Optional charset = body.getEncoding(); -boolean binary = body.isBinary(); -``` - -**After:** - -```java -// Body is now an interface — use the factory methods -Request.Body body = Request.Body.of("hello", StandardCharsets.UTF_8); - -// To read content, write it to a stream (note: these methods throw checked IOException): -byte[] bytes = body.writeToByteArray(); -String str = body.writeToString(StandardCharsets.UTF_8); -long len = body.contentLength(); // -1 if unknown/streaming -boolean repeatable = body.isRepeatable(); -``` - -**Removed methods on `Request.Body`:** - -| Removed | Replacement | -|---------------------------------|---------------------------------------------------------------------------------------| -| `Body.create(String)` | `Body.of(String)` | -| `Body.create(String, Charset)` | `Body.of(String, Charset)` | -| `Body.create(byte[])` | `Body.of(byte[])` | -| `Body.create(byte[], Charset)` | `Body.of(byte[], Charset)` | -| `Body.encoded(byte[], Charset)` | `Body.of(byte[], Charset)` | -| `Body.empty()` | Pass `null` for no body | -| `body.asBytes()` | `body.writeToByteArray()` | -| `body.asString()` | `body.writeToString(charset)` | -| `body.length()` → `int` | `body.contentLength()` → `long` (returns `-1` if unknown) | -| `body.getEncoding()` | Read charset from `Content-Type` header | -| `body.isBinary()` | No direct replacement; `body.isRepeatable()` may be useful depending on your use case | - ---- - -### 2. `Request.body()` now returns `Optional` - -**Before:** - -```java -byte[] body = request.body(); // nullable byte[] -if (body != null) { - out.write(body); -} -``` - -**After:** - -```java -// writeTo(OutputStream) throws checked IOException, so a plain Consumer lambda -// (as used in Optional.ifPresent) cannot propagate it. Use an explicit if-block instead. -Optional body = request.body(); -if (body.isPresent()) { - body.get().writeTo(out); -} -``` - ---- - -### 3. `Request.length()` removed - -**Before:** - -```java -int length = request.length(); -``` - -**After:** - -```java -// contentLength() does not throw, so Optional.map is safe here. -long length = request.body() - .map(Request.Body::contentLength) - .orElse(0L); -``` - ---- - -### 4. `Request.charset()` removed - -**Before:** - -```java -Charset charset = request.charset(); -``` - -**After:** Read the charset from the `Content-Type` request header. There is no longer a charset field on `Request` -itself. - ---- - -### 5. `Request.isBinary()` removed - -**Before:** - -```java -boolean binary = request.isBinary(); -``` - -**After:** - -```java -// There is no direct replacement for request.isBinary(). -// Depending on why you were checking it, request body repeatability may be useful: -boolean repeatable = request.body() - .map(Request.Body::isRepeatable) - .orElse(false); -``` - ---- - -### 6. `Request.create(...)` overloads removed - -The `byte[]` + `Charset`-based `Request.create(...)` overloads have been removed. - -**Before:** - -```java -Request.create(HttpMethod.GET, url, headers, bodyBytes, charset); -Request.create(HttpMethod.GET, url, headers, bodyBytes, charset, requestTemplate); -// Deprecated String-based variant: -Request.create("GET", url, headers, bodyBytes, charset); -``` - -**After:** - -```java -// With a body: -Request.create(HttpMethod.GET, url, headers, Request.Body.of(bodyBytes), requestTemplate); - -// Without a body: -Request.create(HttpMethod.GET, url, headers, null, null); -``` - ---- - -### 7. `RequestTemplate` API changes - -#### `RequestTemplate.body(String)` removed - -**Before:** - -```java -template.body("hello world"); -``` - -**After:** - -```java -template.body(Request.Body.of("hello world")); -``` - -#### `RequestTemplate.body(byte[], Charset)` deprecated - -**Before:** - -```java -template.body(bytes, StandardCharsets.UTF_8); -``` - -**After:** - -```java -template.body(Request.Body.of(bytes, StandardCharsets.UTF_8)); -// or, if charset is irrelevant for your use case: -template.body(Request.Body.of(bytes)); -``` - -#### `RequestTemplate.body()` (returns `byte[]`) removed - -**Before:** - -```java -byte[] body = template.body(); -``` - -**After:** - -```java -// writeToByteArray() throws checked IOException, so a plain Function lambda -// (as used in Optional.map) cannot propagate it. Use an explicit if-block instead. -byte[] body = null; -Optional requestBody = template.requestBody(); -if (requestBody.isPresent()) { - body = requestBody.get().writeToByteArray(); -} -``` - -#### `RequestTemplate.requestBody()` is no longer `@Deprecated` - -The method now returns `Optional` and is the primary accessor. - -#### `RequestTemplate.requestCharset()` removed - -**Before:** - -```java -Charset charset = template.requestCharset(); -``` - -**After:** Read charset from the `Content-Type` header. There is no longer a charset tracked on the template itself. - ---- - -### 8. Custom `Encoder` implementations - -If you implement a custom `Encoder`, update calls to `template.body(...)`: - -**Before:** - -```java -template.body(serialized.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); -// or -template.body(serialized); -``` - -**After:** - -```java -template.body(Request.Body.of(serialized, StandardCharsets.UTF_8)); -// or, for UTF-8 strings: -template.body(Request.Body.of(serialized)); -``` - ---- - -### 9. Custom `Client` implementations - -If you implement a custom `Client`, update how you write the request body: - -**Before:** - -```java -byte[] body = request.body(); -if (body != null) { - outputStream.write(body); -} -``` - -**After:** - -```java -// writeTo(OutputStream) throws checked IOException, so a plain Consumer lambda -// (as used in Optional.ifPresent) cannot propagate it. Use an explicit if-block instead. -Optional body = request.body(); -if (body.isPresent()) { - body.get().writeTo(outputStream); -} -``` - -For retry-capable clients, check `body.isRepeatable()` before attempting a retry — non-repeatable (streaming) bodies -cannot be re-sent. - ---- - -### 10. `FeignException.errorReading(...)` — request body no longer captured - -`FeignException` no longer captures the request body when a read error occurs, because the body may be a non-repeatable -stream. Code asserting `exception.contentUTF8()` returns the request body must be updated: - -**Before:** - -```java -assertThat(exception.contentUTF8()).isEqualTo("Request body"); -``` - -**After:** - -```java -assertThat(exception.contentUTF8()).isEmpty(); -``` - ---- - -### 11. `mock` module — `RequestKey` no longer stores `Charset` - -**Before:** - -```java -RequestKey.builder(...).charset(StandardCharsets.UTF_8).build(); -``` - -**After:** The `charset(Charset)` method has been removed from `RequestKey.Builder`. Remove it from your mock setup -code. - ---- - -### 12. `Request.Body` no longer implements `Serializable` - -`feign.Request.Body` previously implemented `java.io.Serializable`. This has been removed. -If you were serializing `Request.Body` objects (e.g., for caching or distributed tracing), you will need an alternative -serialization strategy. - ---- - -### 13. Vert.x integration — `VertxFeign.Builder` now requires `.vertx(Vertx)` - -**Before:** - -```java -VertxFeign.builder() - .webClient(webClient) - .target(MyApi.class, url); -``` - -**After:** - -```java -VertxFeign.builder() - .vertx(vertx) // required — NPE with descriptive message if missing - .webClient(webClient) - .target(MyApi.class, url); -``` - ---- - -## Implementing a Custom Streaming Body - -If you want to stream a body (e.g., from a file or `InputStream`), implement `Request.Body` directly. Because -`writeTo(OutputStream)` itself declares `throws IOException`, the lambda **can** propagate it freely — the restriction -only applies to standard functional interfaces (`Consumer`, `Function`, etc.) that don't -declare checked exceptions. - -```java -// One-shot InputStream — non-repeatable (isRepeatable() defaults to false) -Request.Body streamingBody = outputStream -> { - try (InputStream in = Files.newInputStream(path)) { - byte[] buffer = new byte[8192]; - int read; - while ((read = in.read(buffer)) != -1) { - outputStream.write(buffer, 0, read); - } - // or, with Java 9+: - // in.transferTo(outputStream); - } - // IOException propagates naturally — no try-catch needed here -}; -template.body(streamingBody); -``` - -For a repeatable streaming body (e.g., backed by a file that can be re-read): - -```java -public class FileBody implements Request.Body { - private final Path path; - - public FileBody(Path path) { - this.path = path; - } - - @Override - public void writeTo(OutputStream out) throws IOException { - try (InputStream in = Files.newInputStream(path)) { - byte[] buffer = new byte[8192]; - int read; - while ((read = in.read(buffer)) != -1) { - out.write(buffer, 0, read); - } - // or, with Java 9+: - // in.transferTo(out); - } - } - - @Override - public boolean isRepeatable() { - return true; - } - - @Override - public long contentLength() { - try { - return Files.size(path); - } catch (IOException e) { - return super.contentLength(); // returns -1 - } - } -} -``` - ---- - -## Spring Cloud OpenFeign Compatibility - -`RequestTemplate#body(byte[], Charset)` is kept `@Deprecated` for backward compatibility with -`spring-cloud-openfeign-core`. Spring Cloud OpenFeign users are not required to make any changes immediately, but should -migrate to `body(Request.Body)` once the Spring team provides an updated release. diff --git a/annotation-error-decoder/src/main/java/feign/error/ExceptionGenerator.java b/annotation-error-decoder/src/main/java/feign/error/ExceptionGenerator.java index 23d5f317b4..2d2c0fe74b 100644 --- a/annotation-error-decoder/src/main/java/feign/error/ExceptionGenerator.java +++ b/annotation-error-decoder/src/main/java/feign/error/ExceptionGenerator.java @@ -45,7 +45,9 @@ class ExceptionGenerator { .status(500) .body((Response.Body) null) .headers(testHeaders) - .request(Request.create(Request.HttpMethod.GET, "http://test", testHeaders, null, null)) + .request( + Request.create( + Request.HttpMethod.GET, "http://test", testHeaders, Request.Body.empty(), null)) .build(); } diff --git a/annotation-error-decoder/src/test/java/feign/error/AbstractAnnotationErrorDecoderTest.java b/annotation-error-decoder/src/test/java/feign/error/AbstractAnnotationErrorDecoderTest.java index 1d7375b30e..e66a25a9f4 100644 --- a/annotation-error-decoder/src/test/java/feign/error/AbstractAnnotationErrorDecoderTest.java +++ b/annotation-error-decoder/src/test/java/feign/error/AbstractAnnotationErrorDecoderTest.java @@ -45,7 +45,9 @@ Response testResponse(int status, String body, Map> h .status(status) .body(body, StandardCharsets.UTF_8) .headers(headers) - .request(Request.create(Request.HttpMethod.GET, "http://test", headers, null, null)) + .request( + Request.create( + Request.HttpMethod.GET, "http://test", headers, Request.Body.empty(), null)) .build(); } } diff --git a/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderExceptionConstructorsTest.java b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderExceptionConstructorsTest.java index 82f0d9231c..9728a46f5e 100644 --- a/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderExceptionConstructorsTest.java +++ b/annotation-error-decoder/src/test/java/feign/error/AnnotationErrorDecoderExceptionConstructorsTest.java @@ -17,6 +17,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import feign.Request; import feign.codec.DefaultDecoder; import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors; import feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors.DeclaredDefaultConstructorException; @@ -55,7 +56,11 @@ public class AnnotationErrorDecoderExceptionConstructorsTest private static final String NON_NULL_BODY = "A GIVEN BODY"; private static final feign.Request REQUEST = feign.Request.create( - feign.Request.HttpMethod.GET, "http://test", Collections.emptyMap(), null, null); + feign.Request.HttpMethod.GET, + "http://test", + Collections.emptyMap(), + Request.Body.empty(), + null); private static final feign.Request NO_REQUEST = null; private static final Map> NON_NULL_HEADERS = new HashMap<>(); private static final Map> NO_HEADERS = null; diff --git a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java index ebe9ad7188..76fbc1e1b3 100644 --- a/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java +++ b/benchmark/src/main/java/feign/benchmark/DecoderIteratorsBenchmark.java @@ -82,7 +82,7 @@ public void buildResponse() { Response.builder() .status(200) .reason("OK") - .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, null)) + .request(Request.create(HttpMethod.GET, "/", Collections.emptyMap(), null, Util.UTF_8)) .headers(Collections.emptyMap()) .body(carsJson(Integer.parseInt(size)), Util.UTF_8) .build(); diff --git a/core/src/main/java/feign/DefaultClient.java b/core/src/main/java/feign/DefaultClient.java index ec0df4644b..d8e02fb4ad 100644 --- a/core/src/main/java/feign/DefaultClient.java +++ b/core/src/main/java/feign/DefaultClient.java @@ -32,7 +32,6 @@ import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.TreeMap; import java.util.zip.DeflaterOutputStream; import java.util.zip.GZIPInputStream; @@ -188,9 +187,9 @@ else if (field.equals(ACCEPT_ENCODING)) { connection.addRequestProperty("Accept", "*/*"); } - Optional body = request.body(); + byte[] body = request.body(); - if (body.isPresent()) { + if (body != null) { /* * Ignore disableRequestBuffering flag if the empty body was set, to ensure that internal * retry logic applies to such requests. @@ -210,7 +209,7 @@ else if (field.equals(ACCEPT_ENCODING)) { out = new DeflaterOutputStream(out); } try { - body.get().writeTo(out); + out.write(body); } finally { try { out.close(); @@ -219,7 +218,7 @@ else if (field.equals(ACCEPT_ENCODING)) { } } - if (!body.isPresent() && request.httpMethod().isWithBody()) { + if (body == null && request.httpMethod().isWithBody()) { // To use this Header, set 'sun.net.http.allowRestrictedHeaders' property true. connection.addRequestProperty("Content-Length", "0"); } diff --git a/core/src/main/java/feign/DefaultContract.java b/core/src/main/java/feign/DefaultContract.java index a59b1183c9..79edadde82 100644 --- a/core/src/main/java/feign/DefaultContract.java +++ b/core/src/main/java/feign/DefaultContract.java @@ -76,7 +76,7 @@ public DefaultContract() { "Body annotation was empty on method %s.", data.configKey()); if (body.indexOf('{') == -1) { - data.template().body(Request.Body.of(body)); + data.template().body(body); } else { data.template().bodyTemplate(body); } diff --git a/core/src/main/java/feign/FeignException.java b/core/src/main/java/feign/FeignException.java index f053591c96..b7ea794cae 100644 --- a/core/src/main/java/feign/FeignException.java +++ b/core/src/main/java/feign/FeignException.java @@ -15,9 +15,7 @@ */ package feign; -import static feign.Util.UTF_8; -import static feign.Util.caseInsensitiveCopyOf; -import static feign.Util.checkNotNull; +import static feign.Util.*; import static java.lang.String.format; import static java.util.regex.Pattern.CASE_INSENSITIVE; @@ -184,7 +182,7 @@ static FeignException errorReading(Request request, Response response, IOExcepti format("%s reading %s %s", cause.getMessage(), request.httpMethod(), request.url()), request, cause, - null, + request.body(), request.headers()); } diff --git a/core/src/main/java/feign/Logger.java b/core/src/main/java/feign/Logger.java index ae297d3861..a460251a6a 100644 --- a/core/src/main/java/feign/Logger.java +++ b/core/src/main/java/feign/Logger.java @@ -78,18 +78,16 @@ protected void logRequest(String configKey, Level logLevel, Request request) { } } - long bodyLength = - request - .body() - .map( - body -> { - if (logLevel.ordinal() >= Level.FULL.ordinal()) { - log(configKey, ""); // CRLF - log(configKey, "%s", body.toString()); - } - return body.contentLength(); - }) - .orElse(0L); + int bodyLength = 0; + if (request.body() != null) { + bodyLength = request.length(); + if (logLevel.ordinal() >= Level.FULL.ordinal()) { + String bodyText = + request.charset() != null ? new String(request.body(), request.charset()) : null; + log(configKey, ""); // CRLF + log(configKey, "%s", bodyText != null ? bodyText : "Binary data"); + } + } log(configKey, "---> END HTTP (%s-byte body)", bodyLength); } } diff --git a/core/src/main/java/feign/Request.java b/core/src/main/java/feign/Request.java index 00ff8ad039..a3627a164d 100644 --- a/core/src/main/java/feign/Request.java +++ b/core/src/main/java/feign/Request.java @@ -19,30 +19,150 @@ import static feign.Util.getThreadIdentifier; import static feign.Util.valuesOrEmpty; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; import java.io.Serializable; import java.net.HttpURLConnection; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; /** An immutable request to an http server. */ public final class Request implements Serializable { + + public enum HttpMethod { + GET, + HEAD, + POST(true), + PUT(true), + DELETE, + CONNECT, + OPTIONS, + TRACE, + PATCH(true); + + private final boolean withBody; + + HttpMethod() { + this(false); + } + + HttpMethod(boolean withBody) { + this.withBody = withBody; + } + + public boolean isWithBody() { + return this.withBody; + } + } + + public enum ProtocolVersion { + HTTP_1_0("HTTP/1.0"), + HTTP_1_1("HTTP/1.1"), + HTTP_2("HTTP/2.0"), + MOCK; + + final String protocolVersion; + + ProtocolVersion() { + protocolVersion = name(); + } + + ProtocolVersion(String protocolVersion) { + this.protocolVersion = protocolVersion; + } + + @Override + public String toString() { + return protocolVersion; + } + } + + /** + * No parameters can be null except {@code body} and {@code charset}. All parameters must be + * effectively immutable, via safe copies, not mutating or otherwise. + * + * @deprecated {@link #create(HttpMethod, String, Map, byte[], Charset)} + */ + @Deprecated + public static Request create( + String method, + String url, + Map> headers, + byte[] body, + Charset charset) { + checkNotNull(method, "httpMethod of %s", method); + final HttpMethod httpMethod = HttpMethod.valueOf(method.toUpperCase()); + return create(httpMethod, url, headers, body, charset, null); + } + + /** + * Builds a Request. All parameters must be effectively immutable, via safe copies. + * + * @param httpMethod for the request. + * @param url for the request. + * @param headers to include. + * @param body of the request, can be {@literal null} + * @param charset of the request, can be {@literal null} + * @return a Request + */ + @Deprecated + public static Request create( + HttpMethod httpMethod, + String url, + Map> headers, + byte[] body, + Charset charset) { + return create(httpMethod, url, headers, Body.create(body, charset), null); + } + + /** + * Builds a Request. All parameters must be effectively immutable, via safe copies. + * + * @param httpMethod for the request. + * @param url for the request. + * @param headers to include. + * @param body of the request, can be {@literal null} + * @param charset of the request, can be {@literal null} + * @return a Request + */ + public static Request create( + HttpMethod httpMethod, + String url, + Map> headers, + byte[] body, + Charset charset, + RequestTemplate requestTemplate) { + return create(httpMethod, url, headers, Body.create(body, charset), requestTemplate); + } + + /** + * Builds a Request. All parameters must be effectively immutable, via safe copies. + * + * @param httpMethod for the request. + * @param url for the request. + * @param headers to include. + * @param body of the request, can be {@literal null} + * @return a Request + */ + public static Request create( + HttpMethod httpMethod, + String url, + Map> headers, + Body body, + RequestTemplate requestTemplate) { + return new Request(httpMethod, url, headers, body, requestTemplate); + } + private final HttpMethod httpMethod; private final String url; private final Map> headers; - private final transient Body body; + private final Body body; private final RequestTemplate requestTemplate; private final ProtocolVersion protocolVersion; @@ -70,40 +190,41 @@ public final class Request implements Serializable { } /** - * Builds a Request. All parameters must be effectively immutable, via safe copies. + * Http Method for this request. * - * @param httpMethod for the request. - * @param url for the request. - * @param headers to include. - * @param body of the request, can be {@literal null} - * @return a Request + * @return the HttpMethod string + * @deprecated @see {@link #httpMethod()} */ - public static Request create( - HttpMethod httpMethod, - String url, - Map> headers, - Body body, - RequestTemplate requestTemplate) { - return new Request(httpMethod, url, headers, body, requestTemplate); + @Deprecated + public String method() { + return httpMethod.name(); } /** - * Returns the body of the request, if any. + * Http Method for the request. * - * @return the body of the request, if any + * @return the HttpMethod. */ - public Optional body() { - return Optional.ofNullable(body); + public HttpMethod httpMethod() { + return this.httpMethod; } /** - * Add new entries to request Headers. It overrides existing entries + * URL for the request. * - * @param key - * @param values + * @return URL as a String. */ - public void header(String key, Collection values) { - headers.put(key, values); + public String url() { + return url; + } + + /** + * Request Headers. + * + * @return the request headers. + */ + public Map> headers() { + return Collections.unmodifiableMap(headers); } /** @@ -117,44 +238,54 @@ public void header(String key, String value) { } /** - * Request Headers. + * Add new entries to request Headers. It overrides existing entries * - * @return the request headers. + * @param key + * @param values */ - public Map> headers() { - return Collections.unmodifiableMap(headers); + public void header(String key, Collection values) { + headers.put(key, values); } /** - * Http Method for the request. + * Charset of the request. * - * @return the HttpMethod. + * @return the current character set for the request, may be {@literal null} for binary data. */ - public HttpMethod httpMethod() { - return this.httpMethod; + public Charset charset() { + return body.encoding; } /** - * Request HTTP protocol version + * If present, this is the replayable body to send to the server. In some cases, this may be + * interpretable as text. * - * @return HTTP protocol version + * @see #charset() */ - public ProtocolVersion protocolVersion() { - return protocolVersion; + public byte[] body() { + return body.data; } - @Experimental - public RequestTemplate requestTemplate() { - return this.requestTemplate; + public boolean isBinary() { + return body.isBinary(); } /** - * URL for the request. + * Request Length. * - * @return URL as a String. + * @return size of the request body. */ - public String url() { - return url; + public int length() { + return this.body.length(); + } + + /** + * Request HTTP protocol version + * + * @return HTTP protocol version + */ + public ProtocolVersion protocolVersion() { + return protocolVersion; } /** @@ -178,185 +309,51 @@ public String toString() { } } if (body != null) { - builder.append('\n').append(body); + builder.append('\n').append(body.asString()); } return builder.toString(); } - public enum HttpMethod { - GET, - HEAD, - POST(true), - PUT(true), - DELETE, - CONNECT, - OPTIONS, - TRACE, - PATCH(true); - - private final boolean withBody; - - HttpMethod() { - this(false); - } - - HttpMethod(boolean withBody) { - this.withBody = withBody; - } - - public boolean isWithBody() { - return this.withBody; - } - } - - public enum ProtocolVersion { - HTTP_1_0("HTTP/1.0"), - HTTP_1_1("HTTP/1.1"), - HTTP_2("HTTP/2.0"), - MOCK; - - final String protocolVersion; - - ProtocolVersion() { - protocolVersion = name(); - } - - ProtocolVersion(String protocolVersion) { - this.protocolVersion = protocolVersion; - } - - @Override - public String toString() { - return protocolVersion; - } - } - /** - * Request Body - * - *

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

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

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

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

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

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

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

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

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

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

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

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

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

This method uses {@link InputStream#transferTo(OutputStream)} to copy all the data, and will - * then attempt to close both streams asynchronously. Some Java compilers might not detect that - * the streams will be safely closed and will issue leak warnings. - * - * @param source InputStream to drain - * @param sink WriteStream to pipe data to - * @param handler a handler that will be called when all the data have been written and the - * streams closed, or if an {@link IOException} has occurred. - */ - public void pipeFromInput( - InputStream source, WriteStream sink, Handler> handler) { - pipeFromInput(source, sink).onComplete(handler); - } - - /** - * Propagate an out-of-band error (likely generated or handled by the code that feeds the output - * stream) to the end of the read stream to let them know that the result is not going to be good. - * - * @param t error to be propagated down the stream - */ - public void sendError(Throwable t) { - context.executeBlocking( - () -> { - errorHandler.handle(t); - return null; - }); - } - - /* ReadStream stuff */ - - /** - * {@inheritDoc} - * - * @param handler {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public OutputToReadStream exceptionHandler(Handler handler) { - // we are usually not propagating exceptions as OutputStream has no mechanism for propagating - // exceptions down, - // except when wrapping an input stream, in which case we can forward InputStream read errors to - // the error handler. - errorHandler = Objects.requireNonNullElse(handler, t -> {}); - return this; - } - - /** - * {@inheritDoc} - * - * @param handler {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public OutputToReadStream handler(Handler handler) { - this.dataHandler = Objects.requireNonNullElse(handler, d -> {}); - return this; - } - - /** - * {@inheritDoc} - * - * @return {@inheritDoc} - */ - @Override - public OutputToReadStream pause() { - paused.getAndSet(new CountDownLatch(1)).countDown(); - return this; - } - - /** - * {@inheritDoc} - * - * @return {@inheritDoc} - */ - @Override - public OutputToReadStream resume() { - paused.getAndSet(new CountDownLatch(0)).countDown(); - return this; - } - - /** - * {@inheritDoc} - * - * @param amount {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public OutputToReadStream fetch(long amount) { - resume(); - demand.addAndGet(amount); - return null; - } - - /** - * {@inheritDoc} - * - * @param endHandler {@inheritDoc} - * @return {@inheritDoc} - */ - @Override - public OutputToReadStream endHandler(Handler endHandler) { - this.endHandler = Objects.requireNonNullElse(endHandler, v -> {}); - return this; - } - - /* OutputStream stuff */ - - /** - * {@inheritDoc} - * - * @param b {@inheritDoc} - * @throws IOException {@inheritDoc} - */ - @Override - public synchronized void write(int b) throws IOException { - if (closed) throw new IOException("OutputStream is closed"); - try { - paused.get().await(); - } catch (InterruptedException e) { - throw new IOException("Interrupted a wait for stream to resume", e); - } - push(Buffer.buffer(1).appendByte((byte) (b & 0xFF))); - } - - /** - * {@inheritDoc} - * - * @param b {@inheritDoc} - * @param off {@inheritDoc} - * @param len {@inheritDoc} - * @throws IOException {@inheritDoc} - */ - @Override - public synchronized void write(byte[] b, int off, int len) throws IOException { - if (closed) throw new IOException("OutputStream is closed"); - try { - paused.get().await(); - } catch (InterruptedException e) { - throw new IOException("Interrupted a wait for stream to resume", e); - } - push(Buffer.buffer(len - off).appendBytes(b, off, len)); - } - - /** - * {@inheritDoc} - * - * @throws IOException {@inheritDoc} - */ - @Override - public synchronized void close() throws IOException { - if (closed) return; - closed = true; - try { - paused.get().await(); - } catch (InterruptedException e) { - throw new IOException("Interrupted a wait for stream to resume", e); - } - push(null); - } - - /* Internal implementation */ - - private void push(Buffer data) { - var awaiter = new CountDownLatch(1); - context.runOnContext( - v -> { - try { - if (data == null) // end of stream - endHandler.handle(null); - else dataHandler.handle(data); - } catch (Throwable t) { - errorHandler.handle(t); - } finally { - awaiter.countDown(); - } - }); - try { - awaiter.await(); - } catch (InterruptedException e) { - } - } -} diff --git a/vertx/feign-vertx/src/main/java/feign/vertx/VertxHttpClient.java b/vertx/feign-vertx/src/main/java/feign/vertx/VertxHttpClient.java index 0a00bd2adb..0b196b6ffb 100644 --- a/vertx/feign-vertx/src/main/java/feign/vertx/VertxHttpClient.java +++ b/vertx/feign-vertx/src/main/java/feign/vertx/VertxHttpClient.java @@ -44,7 +44,6 @@ */ @SuppressWarnings("unused") public final class VertxHttpClient { - private final Vertx vertx; private final WebClient webClient; private final long timeout; private final UnaryOperator> requestPreProcessor; @@ -57,15 +56,12 @@ public final class VertxHttpClient { * @param requestPreProcessor request pre-processor */ public VertxHttpClient( - final Vertx vertx, final WebClient webClient, final long timeout, final UnaryOperator> requestPreProcessor) { - checkNotNull(vertx, "Argument vertx must not be null"); checkNotNull(webClient, "Argument webClient must not be null"); checkNotNull(requestPreProcessor, "Argument requestPreProcessor must be not null"); - this.vertx = vertx; this.webClient = webClient; this.timeout = timeout; this.requestPreProcessor = requestPreProcessor; @@ -89,25 +85,9 @@ public Future execute(final Request request) { } final Future> responseFuture = - request - .body() - .map( - body -> { - OutputToReadStream stream = new OutputToReadStream(vertx); - Future> sendStreamFuture = - httpClientRequest.sendStream(stream); - Future writeFuture = - vertx.executeBlocking( - () -> { - try (stream) { - body.writeTo(stream); - } - return null; - }); - return Future.all(sendStreamFuture, writeFuture) - .map(composite -> sendStreamFuture.result()); - }) - .orElseGet(httpClientRequest::send); + request.body() != null + ? httpClientRequest.sendBuffer(Buffer.buffer(request.body())) + : httpClientRequest.send(); return responseFuture.compose( response -> { diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java b/vertx/feign-vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java index dd7f496b6b..5931c7acb4 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/ConnectionsLeakTests.java @@ -81,7 +81,6 @@ void http11NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { HelloServiceAPI client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) @@ -109,7 +108,6 @@ void http2NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { HelloServiceAPI client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java index 88dab3713b..fae9a70e9d 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/Http11ClientReconnectTest.java @@ -38,7 +38,6 @@ protected void createClient(final Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/QueryMapEncoderTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/QueryMapEncoderTest.java index 69e3d5dfd8..39a43a8adb 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/QueryMapEncoderTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/QueryMapEncoderTest.java @@ -55,7 +55,6 @@ void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .queryMapEncoder(new CustomQueryMapEncoder()) diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/RawContractTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/RawContractTest.java index e816622474..48f46b4b49 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/RawContractTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/RawContractTest.java @@ -45,7 +45,6 @@ class RawContractTest extends AbstractFeignVertxTest { static void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(WebClient.create(vertx)) .encoder(new JacksonEncoder(TestUtils.MAPPER)) .decoder(new JacksonDecoder(TestUtils.MAPPER)) diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/RequestPreProcessorTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/RequestPreProcessorTest.java index 2d88afa6c9..87850cb9e3 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/RequestPreProcessorTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/RequestPreProcessorTest.java @@ -47,7 +47,6 @@ void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .requestPreProcessor(req -> req.addQueryParam("version", "v1")) diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/RetryingTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/RetryingTest.java index 89691c9d5a..b294d99363 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/RetryingTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/RetryingTest.java @@ -48,7 +48,6 @@ class RetryingTest extends AbstractFeignVertxTest { static void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(WebClient.create(vertx)) .decoder(new JacksonDecoder(MAPPER)) .retryer(new DefaultRetryer(100, SECONDS.toMillis(1), 5)) diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/TimeoutHandlingTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/TimeoutHandlingTest.java index 5bd97dd4a5..29e7efa6e1 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/TimeoutHandlingTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/TimeoutHandlingTest.java @@ -48,7 +48,6 @@ void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .timeout(1000) diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpClientTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpClientTest.java index 005568436b..eca7765385 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpClientTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpClientTest.java @@ -65,7 +65,6 @@ void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .logger(new Slf4jLogger()) @@ -218,7 +217,6 @@ class WhenMakePostRequest { void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(WebClient.create(vertx)) .encoder(new JacksonEncoder(TestUtils.MAPPER)) .decoder(new JacksonDecoder(TestUtils.MAPPER)) @@ -306,23 +304,6 @@ void whenVertxMissing() { ThrowableAssert.ThrowingCallable instantiateContractForgottenVertx = () -> VertxFeign.builder().target(IcecreamServiceApi.class, wireMock.baseUrl()); - /* Then */ - assertThatCode(instantiateContractForgottenVertx) - .isInstanceOf(NullPointerException.class) - .hasMessage("Vertx instance wasn't provided in VertxFeign builder"); - } - - @Test - @DisplayName("when Vertx WebClient is not provided") - void whenVertxWebClientMissing(Vertx vertx) { - - /* Given */ - ThrowableAssert.ThrowingCallable instantiateContractForgottenVertx = - () -> - VertxFeign.builder() - .vertx(vertx) - .target(IcecreamServiceApi.class, wireMock.baseUrl()); - /* Then */ assertThatCode(instantiateContractForgottenVertx) .isInstanceOf(NullPointerException.class) @@ -337,7 +318,6 @@ void whenTryToInstantiateBrokenContract(Vertx vertx) { ThrowableAssert.ThrowingCallable instantiateBrokenContract = () -> VertxFeign.builder() - .vertx(vertx) .webClient(WebClient.create(vertx)) .target(IcecreamServiceApiBroken.class, wireMock.baseUrl()); diff --git a/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpOptionsTest.java b/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpOptionsTest.java index 21eefbe9ca..626376a347 100644 --- a/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpOptionsTest.java +++ b/vertx/feign-vertx/src/test/java/feign/vertx/VertxHttpOptionsTest.java @@ -65,7 +65,6 @@ void httpClientOptions(Vertx vertx, VertxTestContext testContext) { IcecreamServiceApi client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .logger(new Slf4jLogger()) @@ -87,7 +86,6 @@ void requestOptions(Vertx vertx, VertxTestContext testContext) { IcecreamServiceApi client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .logger(new Slf4jLogger()) diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/ConnectionsLeakTests.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/ConnectionsLeakTests.java index 4dcde50338..b76b751ed0 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/ConnectionsLeakTests.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/ConnectionsLeakTests.java @@ -84,7 +84,6 @@ void http11NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { HelloServiceAPI client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) @@ -112,7 +111,6 @@ void http2NoConnectionLeak(Vertx vertx, VertxTestContext testContext) { HelloServiceAPI client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http11ClientReconnectTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http11ClientReconnectTest.java index bb6edaf4a8..207ae8a89e 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http11ClientReconnectTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/Http11ClientReconnectTest.java @@ -36,7 +36,6 @@ protected void createClient(final Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/QueryMapEncoderTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/QueryMapEncoderTest.java index 69e3d5dfd8..39a43a8adb 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/QueryMapEncoderTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/QueryMapEncoderTest.java @@ -55,7 +55,6 @@ void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .queryMapEncoder(new CustomQueryMapEncoder()) diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/RawContractTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/RawContractTest.java index e816622474..48f46b4b49 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/RawContractTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/RawContractTest.java @@ -45,7 +45,6 @@ class RawContractTest extends AbstractFeignVertxTest { static void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(WebClient.create(vertx)) .encoder(new JacksonEncoder(TestUtils.MAPPER)) .decoder(new JacksonDecoder(TestUtils.MAPPER)) diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/RequestPreProcessorTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/RequestPreProcessorTest.java index 2d88afa6c9..87850cb9e3 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/RequestPreProcessorTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/RequestPreProcessorTest.java @@ -47,7 +47,6 @@ void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .requestPreProcessor(req -> req.addQueryParam("version", "v1")) diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/RetryingTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/RetryingTest.java index 89691c9d5a..b294d99363 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/RetryingTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/RetryingTest.java @@ -48,7 +48,6 @@ class RetryingTest extends AbstractFeignVertxTest { static void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(WebClient.create(vertx)) .decoder(new JacksonDecoder(MAPPER)) .retryer(new DefaultRetryer(100, SECONDS.toMillis(1), 5)) diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/TimeoutHandlingTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/TimeoutHandlingTest.java index 5bd97dd4a5..29e7efa6e1 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/TimeoutHandlingTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/TimeoutHandlingTest.java @@ -48,7 +48,6 @@ void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .timeout(1000) diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpClientTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpClientTest.java index 005568436b..eca7765385 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpClientTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpClientTest.java @@ -65,7 +65,6 @@ void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .logger(new Slf4jLogger()) @@ -218,7 +217,6 @@ class WhenMakePostRequest { void createClient(Vertx vertx) { client = VertxFeign.builder() - .vertx(vertx) .webClient(WebClient.create(vertx)) .encoder(new JacksonEncoder(TestUtils.MAPPER)) .decoder(new JacksonDecoder(TestUtils.MAPPER)) @@ -306,23 +304,6 @@ void whenVertxMissing() { ThrowableAssert.ThrowingCallable instantiateContractForgottenVertx = () -> VertxFeign.builder().target(IcecreamServiceApi.class, wireMock.baseUrl()); - /* Then */ - assertThatCode(instantiateContractForgottenVertx) - .isInstanceOf(NullPointerException.class) - .hasMessage("Vertx instance wasn't provided in VertxFeign builder"); - } - - @Test - @DisplayName("when Vertx WebClient is not provided") - void whenVertxWebClientMissing(Vertx vertx) { - - /* Given */ - ThrowableAssert.ThrowingCallable instantiateContractForgottenVertx = - () -> - VertxFeign.builder() - .vertx(vertx) - .target(IcecreamServiceApi.class, wireMock.baseUrl()); - /* Then */ assertThatCode(instantiateContractForgottenVertx) .isInstanceOf(NullPointerException.class) @@ -337,7 +318,6 @@ void whenTryToInstantiateBrokenContract(Vertx vertx) { ThrowableAssert.ThrowingCallable instantiateBrokenContract = () -> VertxFeign.builder() - .vertx(vertx) .webClient(WebClient.create(vertx)) .target(IcecreamServiceApiBroken.class, wireMock.baseUrl()); diff --git a/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpOptionsTest.java b/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpOptionsTest.java index 2ca7a54cb6..0e83d3ea62 100644 --- a/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpOptionsTest.java +++ b/vertx/feign-vertx4-test/src/test/java/feign/vertx/VertxHttpOptionsTest.java @@ -64,7 +64,6 @@ void httpClientOptions(Vertx vertx, VertxTestContext testContext) { IcecreamServiceApi client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .logger(new Slf4jLogger()) @@ -86,7 +85,6 @@ void requestOptions(Vertx vertx, VertxTestContext testContext) { IcecreamServiceApi client = VertxFeign.builder() - .vertx(vertx) .webClient(webClient) .decoder(new JacksonDecoder(TestUtils.MAPPER)) .logger(new Slf4jLogger()) From 2c5582154e69e5eb429a63836043bf55cebac9cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:56:32 +0000 Subject: [PATCH 06/51] build(deps): Bump jackson.version from 2.21.3 to 2.22.0 Bumps `jackson.version` from 2.21.3 to 2.22.0. Updates `com.fasterxml.jackson:jackson-bom` from 2.21.3 to 2.22.0 - [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.21.3...jackson-bom-2.22.0) Updates `com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider` from 2.21.3 to 2.22.0 --- updated-dependencies: - dependency-name: com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider dependency-version: 2.22.0 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: com.fasterxml.jackson:jackson-bom dependency-version: 2.22.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- vertx/feign-vertx/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 3b613d5f81..1eb4cf6aaa 100644 --- a/pom.xml +++ b/pom.xml @@ -174,7 +174,7 @@ 4.0.6 6.1.0 - 2.21.3 + 2.22.0 3.1.3 3.27.7 5.23.0 diff --git a/vertx/feign-vertx/pom.xml b/vertx/feign-vertx/pom.xml index 718138ecdc..518822a8b5 100644 --- a/vertx/feign-vertx/pom.xml +++ b/vertx/feign-vertx/pom.xml @@ -31,7 +31,7 @@ 11 - 2.21.3 + 2.22.0 From 83a98e32c7cff8cd3d8c476b56371ff95081faf8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:58:05 +0000 Subject: [PATCH 07/51] build(deps): Bump org.hibernate.validator:hibernate-validator Bumps [org.hibernate.validator:hibernate-validator](https://github.com/hibernate/hibernate-validator) from 6.2.5.Final to 9.1.0.Final. - [Release notes](https://github.com/hibernate/hibernate-validator/releases) - [Changelog](https://github.com/hibernate/hibernate-validator/blob/main/changelog.txt) - [Commits](https://github.com/hibernate/hibernate-validator/compare/6.2.5.Final...9.1.0.Final) --- updated-dependencies: - dependency-name: org.hibernate.validator:hibernate-validator dependency-version: 9.1.0.Final dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 3b613d5f81..15aad04c41 100644 --- a/pom.xml +++ b/pom.xml @@ -237,8 +237,8 @@ 2.3.1 2.0.1.Final 3.1.1 - 6.2.5.Final - 8.0.1.Final + 9.1.0.Final + 9.1.0.Final 3.0.4 6.0.0 1.19.4 From 0684947cefc21b1f8ebb36b1bc4238d5bcb7ee92 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 05:12:38 +0000 Subject: [PATCH 08/51] build(deps): Bump org.springframework.cloud:spring-cloud-dependencies Bumps [org.springframework.cloud:spring-cloud-dependencies](https://github.com/spring-cloud/spring-cloud-release) from 2025.1.1 to 2025.1.2. - [Release notes](https://github.com/spring-cloud/spring-cloud-release/releases) - [Commits](https://github.com/spring-cloud/spring-cloud-release/compare/v2025.1.1...v2025.1.2) --- updated-dependencies: - dependency-name: org.springframework.cloud:spring-cloud-dependencies dependency-version: 2025.1.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3b613d5f81..fac91fd459 100644 --- a/pom.xml +++ b/pom.xml @@ -251,7 +251,7 @@ 2.1.1 1.5.3 3.0.2 - 2025.1.1 + 2025.1.2 5.0.1 7.0.7 7.0.7 From ee44507f4c017377d2fdffca3d8fb0c57c025b83 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 05:13:11 +0000 Subject: [PATCH 09/51] build(deps-dev): Bump jersey.version from 3.1.11 to 3.1.12 in /jaxrs3 Bumps `jersey.version` from 3.1.11 to 3.1.12. Updates `org.glassfish.jersey.core:jersey-client` from 3.1.11 to 3.1.12 Updates `org.glassfish.jersey.inject:jersey-hk2` from 3.1.11 to 3.1.12 --- updated-dependencies: - dependency-name: org.glassfish.jersey.core:jersey-client dependency-version: 3.1.12 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: org.glassfish.jersey.inject:jersey-hk2 dependency-version: 3.1.12 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- jaxrs3/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jaxrs3/pom.xml b/jaxrs3/pom.xml index b5dabe1d19..0e3193bb0d 100644 --- a/jaxrs3/pom.xml +++ b/jaxrs3/pom.xml @@ -31,7 +31,7 @@ 11 - 3.1.11 + 3.1.12 From 77342a7907bba99ebbca2ad3cd1af4ec0f190a2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 05:24:57 +0000 Subject: [PATCH 10/51] build(deps-dev): Bump org.springframework.cloud:spring-cloud-starter-openfeign Bumps [org.springframework.cloud:spring-cloud-starter-openfeign](https://github.com/spring-cloud/spring-cloud-openfeign) from 5.0.1 to 5.0.2. - [Release notes](https://github.com/spring-cloud/spring-cloud-openfeign/releases) - [Commits](https://github.com/spring-cloud/spring-cloud-openfeign/compare/v5.0.1...v5.0.2) --- updated-dependencies: - dependency-name: org.springframework.cloud:spring-cloud-starter-openfeign dependency-version: 5.0.2 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fac91fd459..63728c151d 100644 --- a/pom.xml +++ b/pom.xml @@ -252,7 +252,7 @@ 1.5.3 3.0.2 2025.1.2 - 5.0.1 + 5.0.2 7.0.7 7.0.7 2.4.1.Final From 0a5a9983f2696cc0a57ea960b8d297cff7a78abf Mon Sep 17 00:00:00 2001 From: velo Date: Sat, 13 Jun 2026 08:17:01 -0300 Subject: [PATCH 11/51] Keep legacy validation on Hibernate Validator 6 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 15aad04c41..826b0a680e 100644 --- a/pom.xml +++ b/pom.xml @@ -237,7 +237,7 @@ 2.3.1 2.0.1.Final 3.1.1 - 9.1.0.Final + 6.2.5.Final 9.1.0.Final 3.0.4 6.0.0 From 61154cb1879dea98b495257ecb088548e0bce200 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 11:51:58 +0000 Subject: [PATCH 12/51] build(deps): Bump tools.jackson:jackson-bom from 3.1.3 to 3.2.0 Bumps [tools.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 3.1.3 to 3.2.0. - [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-3.1.3...jackson-bom-3.2.0) --- updated-dependencies: - dependency-name: tools.jackson:jackson-bom dependency-version: 3.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 28b3673d9b..3038cdeec0 100644 --- a/pom.xml +++ b/pom.xml @@ -175,7 +175,7 @@ 6.1.0 2.22.0 - 3.1.3 + 3.2.0 3.27.7 5.23.0 2.0.61.android8 From 058693793a56a6b8191beed912cd4bccb3c916f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 11:52:09 +0000 Subject: [PATCH 13/51] build(deps): Bump springboot.version from 4.0.6 to 4.1.0 Bumps `springboot.version` from 4.0.6 to 4.1.0. Updates `org.springframework.boot:spring-boot-maven-plugin` from 4.0.6 to 4.1.0 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v4.0.6...v4.1.0) Updates `org.springframework.boot:spring-boot-starter-web` from 4.0.6 to 4.1.0 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v4.0.6...v4.1.0) Updates `org.springframework.boot:spring-boot-starter-test` from 4.0.6 to 4.1.0 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v4.0.6...v4.1.0) --- updated-dependencies: - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-version: 4.1.0 dependency-type: direct:development update-type: version-update:semver-minor - dependency-name: org.springframework.boot:spring-boot-starter-test dependency-version: 4.1.0 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.springframework.boot:spring-boot-starter-web dependency-version: 4.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 28b3673d9b..8bab55f28f 100644 --- a/pom.xml +++ b/pom.xml @@ -171,7 +171,7 @@ 1.15.2 2.0.18 20260522 - 4.0.6 + 4.1.0 6.1.0 2.22.0 From 82992960be9e6ba4d7fe3a49251fd2b26f1959ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 11:57:36 +0000 Subject: [PATCH 14/51] build(deps): Bump com.squareup.okhttp3:okhttp-bom from 5.3.2 to 5.4.0 Bumps [com.squareup.okhttp3:okhttp-bom](https://github.com/square/okhttp) from 5.3.2 to 5.4.0. - [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md) - [Commits](https://github.com/square/okhttp/compare/parent-5.3.2...parent-5.4.0) --- updated-dependencies: - dependency-name: com.squareup.okhttp3:okhttp-bom dependency-version: 5.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 28b3673d9b..9e77247efb 100644 --- a/pom.xml +++ b/pom.xml @@ -164,7 +164,7 @@ ${main.java.version} ${main.java.version} - 5.3.2 + 5.4.0 33.6.0-jre 2.1.0 2.14.0 From a4bcd853d1f99938100b17d3696f3979e129f3b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 11:57:38 +0000 Subject: [PATCH 15/51] build(deps-dev): Bump vertx.version in /vertx/feign-vertx5-test Bumps `vertx.version` from 5.1.1 to 5.1.2. Updates `io.vertx:vertx-junit5` from 5.1.1 to 5.1.2 - [Commits](https://github.com/eclipse-vertx/vertx-junit5/compare/5.1.1...5.1.2) Updates `io.vertx:vertx-web-client` from 5.1.1 to 5.1.2 - [Commits](https://github.com/vert-x3/vertx-web/compare/5.1.1...5.1.2) --- updated-dependencies: - dependency-name: io.vertx:vertx-junit5 dependency-version: 5.1.2 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: io.vertx:vertx-web-client dependency-version: 5.1.2 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- vertx/feign-vertx5-test/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vertx/feign-vertx5-test/pom.xml b/vertx/feign-vertx5-test/pom.xml index 3cd071e05d..0501998954 100644 --- a/vertx/feign-vertx5-test/pom.xml +++ b/vertx/feign-vertx5-test/pom.xml @@ -30,7 +30,7 @@ Tests with Vertx 5.x. - 5.1.1 + 5.1.2 From b5b41dcaada75a0e4d66417c17fa2a56b3c89d4b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 11:57:39 +0000 Subject: [PATCH 16/51] build(deps): Bump reactor.version from 3.8.5 to 3.8.6 Bumps `reactor.version` from 3.8.5 to 3.8.6. Updates `io.projectreactor:reactor-core` from 3.8.5 to 3.8.6 - [Release notes](https://github.com/reactor/reactor-core/releases) - [Commits](https://github.com/reactor/reactor-core/compare/v3.8.5...v3.8.6) Updates `io.projectreactor:reactor-test` from 3.8.5 to 3.8.6 - [Release notes](https://github.com/reactor/reactor-core/releases) - [Commits](https://github.com/reactor/reactor-core/compare/v3.8.5...v3.8.6) --- updated-dependencies: - dependency-name: io.projectreactor:reactor-core dependency-version: 3.8.6 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.projectreactor:reactor-test dependency-version: 3.8.6 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- reactive/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reactive/pom.xml b/reactive/pom.xml index d06c79ff62..9b6da88b97 100644 --- a/reactive/pom.xml +++ b/reactive/pom.xml @@ -29,7 +29,7 @@ Reactive Wrapper for Feign Clients - 3.8.5 + 3.8.6 1.0.4 2.2.21 From 05035c61937992dd07b2aaecd032ce6e8bd41ba2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 11:57:42 +0000 Subject: [PATCH 17/51] build(deps): Bump micrometer.version from 1.16.5 to 1.17.0 Bumps `micrometer.version` from 1.16.5 to 1.17.0. Updates `io.micrometer:micrometer-core` from 1.16.5 to 1.17.0 - [Release notes](https://github.com/micrometer-metrics/micrometer/releases) - [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.16.5...v1.17.0) Updates `io.micrometer:micrometer-test` from 1.16.5 to 1.17.0 - [Release notes](https://github.com/micrometer-metrics/micrometer/releases) - [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.16.5...v1.17.0) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-core dependency-version: 1.17.0 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: io.micrometer:micrometer-test dependency-version: 1.17.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- micrometer/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/micrometer/pom.xml b/micrometer/pom.xml index 6092d53273..c53fb2d6c8 100644 --- a/micrometer/pom.xml +++ b/micrometer/pom.xml @@ -28,7 +28,7 @@ Feign Micrometer Application Metrics - 1.16.5 + 1.17.0 From 02e4448ab1ae18dac31cd2039ccf5bb58768d63a Mon Sep 17 00:00:00 2001 From: alhuda <50839256+alhudz@users.noreply.github.com> Date: Sat, 13 Jun 2026 17:35:32 +0530 Subject: [PATCH 18/51] guard numeric retry-after against long overflow (#3405) Co-authored-by: alhudz Co-authored-by: Marvin --- core/src/main/java/feign/codec/ErrorDecoder.java | 12 ++++++------ .../test/java/feign/codec/RetryAfterDecoderTest.java | 5 +++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/feign/codec/ErrorDecoder.java b/core/src/main/java/feign/codec/ErrorDecoder.java index a2a55dc54a..89e0ac279e 100644 --- a/core/src/main/java/feign/codec/ErrorDecoder.java +++ b/core/src/main/java/feign/codec/ErrorDecoder.java @@ -120,14 +120,14 @@ public Long apply(String retryAfter) { if (retryAfter == null) { return null; } - if (retryAfter.matches("^[0-9]+\\.?0*$")) { - retryAfter = retryAfter.replaceAll("\\.0*$", ""); - long deltaMillis = SECONDS.toMillis(Long.parseLong(retryAfter)); - return currentTimeMillis() + deltaMillis; - } try { + if (retryAfter.matches("^[0-9]+\\.?0*$")) { + retryAfter = retryAfter.replaceAll("\\.0*$", ""); + long deltaMillis = SECONDS.toMillis(Long.parseLong(retryAfter)); + return currentTimeMillis() + deltaMillis; + } return ZonedDateTime.parse(retryAfter, dateTimeFormatter).toInstant().toEpochMilli(); - } catch (NullPointerException | DateTimeParseException ignored) { + } catch (NumberFormatException | NullPointerException | DateTimeParseException ignored) { return null; } } diff --git a/core/src/test/java/feign/codec/RetryAfterDecoderTest.java b/core/src/test/java/feign/codec/RetryAfterDecoderTest.java index 6ee79ae540..0310ce05e8 100644 --- a/core/src/test/java/feign/codec/RetryAfterDecoderTest.java +++ b/core/src/test/java/feign/codec/RetryAfterDecoderTest.java @@ -55,6 +55,11 @@ void relativeSecondsParseDecimalIntegers() throws ParseException { assertThat(decoder.apply("86400.0")).isEqualTo(parseDateTime("Sun, 2 Jan 2000 00:00:00 GMT")); } + @Test + void overflowingRelativeSecondsFailsGracefully() { + assertThat(decoder.apply("99999999999999999999")).isNull(); + } + private Long parseDateTime(String text) { try { return ZonedDateTime.parse(text, RFC_1123_DATE_TIME).toInstant().toEpochMilli(); From cb5554b6149769ab770bd46758d3f434ccfb8ab7 Mon Sep 17 00:00:00 2001 From: Marvin Froeder Date: Sat, 13 Jun 2026 09:11:49 -0300 Subject: [PATCH 19/51] Align Apache HttpClient checked-exception handling with master's #1487 fix Signed-off-by: Marvin Froeder --- .../java/feign/SynchronousMethodHandler.java | 28 +++++++------------ .../feign/httpclient/ApacheHttpClient.java | 8 ++---- .../httpclient/ApacheHttpClientTest.java | 15 ---------- 3 files changed, 12 insertions(+), 39 deletions(-) diff --git a/core/src/main/java/feign/SynchronousMethodHandler.java b/core/src/main/java/feign/SynchronousMethodHandler.java index 4e611a12e4..64b81d43b4 100644 --- a/core/src/main/java/feign/SynchronousMethodHandler.java +++ b/core/src/main/java/feign/SynchronousMethodHandler.java @@ -24,7 +24,6 @@ import feign.interceptor.Invocation; import feign.interceptor.MethodInterceptor; import java.io.IOException; -import java.lang.reflect.Method; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; @@ -76,13 +75,16 @@ private Object runWithRetry(Invocation invocation, Options options) throws Throw retryer.continueOrPropagate(e); } catch (RetryableException th) { Throwable cause = th.getCause(); - if (methodHandlerConfiguration.getPropagationPolicy() == UNWRAP - && cause != null - && (cause instanceof RuntimeException - || cause instanceof Error - || isDeclaredCheckedException( - cause, methodHandlerConfiguration.getMetadata().method()))) { - throw cause; + if (methodHandlerConfiguration.getPropagationPolicy() == UNWRAP && cause != null) { + if (cause instanceof RuntimeException || cause instanceof Error) { + throw cause; + } + for (Class exceptionType : + methodHandlerConfiguration.getMetadata().method().getExceptionTypes()) { + if (exceptionType.isAssignableFrom(cause.getClass())) { + throw cause; + } + } } throw th; } @@ -165,16 +167,6 @@ Options findOptions(Object[] argv) { .getMethodOptions(methodHandlerConfiguration.getMetadata().method().getName())); } - private static boolean isDeclaredCheckedException(Throwable cause, Method method) { - Class[] exceptionTypes = method.getExceptionTypes(); - for (Class exType : exceptionTypes) { - if (exType.isAssignableFrom(cause.getClass())) { - return true; - } - } - return false; - } - static class Factory implements MethodHandler.Factory { private final Client client; diff --git a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java index 5ab2ba459e..010c229cb7 100644 --- a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java +++ b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java @@ -81,12 +81,8 @@ public Response execute(Request request, Request.Options options) throws IOExcep } catch (URISyntaxException e) { throw new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", e); } - try { - HttpResponse httpResponse = client.execute(httpUriRequest); - return toFeignResponse(httpResponse, request); - } catch (Exception e) { - throw new IOException(e); - } + HttpResponse httpResponse = client.execute(httpUriRequest); + return toFeignResponse(httpResponse, request); } HttpUriRequest toHttpUriRequest(Request request, Request.Options options) diff --git a/httpclient/src/test/java/feign/httpclient/ApacheHttpClientTest.java b/httpclient/src/test/java/feign/httpclient/ApacheHttpClientTest.java index 079d70dc0d..4ab3601425 100644 --- a/httpclient/src/test/java/feign/httpclient/ApacheHttpClientTest.java +++ b/httpclient/src/test/java/feign/httpclient/ApacheHttpClientTest.java @@ -112,21 +112,6 @@ void notFollowRedirectIsRespected() throws InterruptedException { assertThat(server.takeRequest().getPath()).isEqualTo("/withOptions"); } - @Test - void redirectWithoutLocationHeader() { - JaxRsTestInterface api = - Feign.builder() - .contract(new JAXRSContract()) - .retryer(Retryer.NEVER_RETRY) - .client(new ApacheHttpClient(HttpClientBuilder.create().build())) - .target(JaxRsTestInterface.class, "http://localhost:" + server.getPort()); - - server.enqueue(new MockResponse().setResponseCode(303)); - RetryableException exception = - assertThrows(RetryableException.class, () -> api.withoutBody("foo")); - assertThat(exception.getMessage()).contains("org.apache.http.client.ClientProtocolException"); - } - private JaxRsTestInterface buildTestInterface() { return Feign.builder() .contract(new JAXRSContract()) From 865a0f6f83e112c0ee7754a2ef0f0b981fa22ff9 Mon Sep 17 00:00:00 2001 From: Marvin Date: Sat, 13 Jun 2026 10:43:37 -0300 Subject: [PATCH 20/51] build(deps): consolidate spring-web/spring-context into single spring.version 7.0.8 (#3413) Signed-off-by: Marvin Froeder --- core/pom.xml | 2 +- form-spring/pom.xml | 2 +- pom.xml | 3 +-- spring/pom.xml | 1 - spring4/pom.xml | 4 ---- 5 files changed, 3 insertions(+), 9 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index 58f79c5871..a99c574c42 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -50,7 +50,7 @@ org.springframework spring-context - ${spring-context.version} + ${spring.version} test diff --git a/form-spring/pom.xml b/form-spring/pom.xml index 18a880fc1a..9d8cee4ad0 100644 --- a/form-spring/pom.xml +++ b/form-spring/pom.xml @@ -58,7 +58,7 @@ org.springframework spring-web - ${spring-web.version} + ${spring.version} compile diff --git a/pom.xml b/pom.xml index fc7a366722..5afb6b3601 100644 --- a/pom.xml +++ b/pom.xml @@ -253,8 +253,7 @@ 3.0.2 2025.1.2 5.0.2 - 7.0.7 - 7.0.7 + 7.0.8 2.4.1.Final 1.18.0 diff --git a/spring/pom.xml b/spring/pom.xml index 540a72cfb2..6c225e178b 100644 --- a/spring/pom.xml +++ b/spring/pom.xml @@ -31,7 +31,6 @@ 17 - 7.0.7 diff --git a/spring4/pom.xml b/spring4/pom.xml index c137a8ce87..faf785491e 100644 --- a/spring4/pom.xml +++ b/spring4/pom.xml @@ -36,10 +36,6 @@ - - 4.3.30.RELEASE - - io.github.openfeign From 2a2344d365540bcf1dedb2dd7f2920f61e85c3b5 Mon Sep 17 00:00:00 2001 From: velo Date: Sat, 13 Jun 2026 13:56:13 -0300 Subject: [PATCH 21/51] prepare release 13.13 --- annotation-error-decoder/pom.xml | 2 +- apt-test-generator/pom.xml | 2 +- benchmark/pom.xml | 2 +- core/pom.xml | 2 +- dropwizard-metrics4/pom.xml | 2 +- dropwizard-metrics5/pom.xml | 2 +- example-github-with-coroutine/pom.xml | 2 +- example-github/pom.xml | 2 +- example-wikipedia-with-springboot/pom.xml | 2 +- example-wikipedia/pom.xml | 2 +- fastjson2/pom.xml | 2 +- feign-bom/pom.xml | 90 +++++++++++------------ form-spring/pom.xml | 2 +- form/pom.xml | 2 +- googlehttpclient/pom.xml | 2 +- graphql-apt/pom.xml | 2 +- graphql/pom.xml | 2 +- gson/pom.xml | 2 +- hc5/pom.xml | 2 +- http-cache/pom.xml | 2 +- httpclient/pom.xml | 2 +- hystrix/pom.xml | 2 +- jackson-jaxb/pom.xml | 2 +- jackson-jr/pom.xml | 2 +- jackson/pom.xml | 2 +- jackson3/pom.xml | 2 +- jakarta/pom.xml | 2 +- java11/pom.xml | 2 +- jaxb-jakarta/pom.xml | 2 +- jaxb/pom.xml | 2 +- jaxrs/pom.xml | 2 +- jaxrs2/pom.xml | 2 +- jaxrs3/pom.xml | 2 +- jaxrs4/pom.xml | 2 +- json/pom.xml | 2 +- kotlin/pom.xml | 2 +- micrometer/pom.xml | 2 +- mock/pom.xml | 2 +- moshi/pom.xml | 2 +- okhttp/pom.xml | 2 +- pom.xml | 2 +- reactive/pom.xml | 2 +- ribbon/pom.xml | 2 +- sax/pom.xml | 2 +- slf4j/pom.xml | 2 +- soap-jakarta/pom.xml | 2 +- soap/pom.xml | 2 +- spring/pom.xml | 2 +- spring4/pom.xml | 2 +- validation-jakarta/pom.xml | 2 +- validation/pom.xml | 2 +- vertx/feign-vertx/pom.xml | 2 +- vertx/feign-vertx4-test/pom.xml | 2 +- vertx/feign-vertx5-test/pom.xml | 2 +- vertx/pom.xml | 2 +- 55 files changed, 99 insertions(+), 99 deletions(-) diff --git a/annotation-error-decoder/pom.xml b/annotation-error-decoder/pom.xml index 25e1be69c0..486fb8ab6f 100644 --- a/annotation-error-decoder/pom.xml +++ b/annotation-error-decoder/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-annotation-error-decoder diff --git a/apt-test-generator/pom.xml b/apt-test-generator/pom.xml index c36dc2789b..22786640b5 100644 --- a/apt-test-generator/pom.xml +++ b/apt-test-generator/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 io.github.openfeign.experimental diff --git a/benchmark/pom.xml b/benchmark/pom.xml index 6f79e261e1..0ec4819be7 100644 --- a/benchmark/pom.xml +++ b/benchmark/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-benchmark diff --git a/core/pom.xml b/core/pom.xml index a99c574c42..c437c066d1 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-core diff --git a/dropwizard-metrics4/pom.xml b/dropwizard-metrics4/pom.xml index b1d584a4fc..fdad3983e7 100644 --- a/dropwizard-metrics4/pom.xml +++ b/dropwizard-metrics4/pom.xml @@ -21,7 +21,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-dropwizard-metrics4 Feign Dropwizard Metrics4 diff --git a/dropwizard-metrics5/pom.xml b/dropwizard-metrics5/pom.xml index 97ebea5884..ba23ee0790 100644 --- a/dropwizard-metrics5/pom.xml +++ b/dropwizard-metrics5/pom.xml @@ -21,7 +21,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-dropwizard-metrics5 Feign Dropwizard Metrics5 diff --git a/example-github-with-coroutine/pom.xml b/example-github-with-coroutine/pom.xml index 671b20e06f..186a1adee0 100644 --- a/example-github-with-coroutine/pom.xml +++ b/example-github-with-coroutine/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-example-github-with-coroutine diff --git a/example-github/pom.xml b/example-github/pom.xml index 2458198a11..52f9bdd403 100644 --- a/example-github/pom.xml +++ b/example-github/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-example-github diff --git a/example-wikipedia-with-springboot/pom.xml b/example-wikipedia-with-springboot/pom.xml index 6424011e8b..d93d37099c 100644 --- a/example-wikipedia-with-springboot/pom.xml +++ b/example-wikipedia-with-springboot/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-example-wikipedia-with-springboot diff --git a/example-wikipedia/pom.xml b/example-wikipedia/pom.xml index 551c2969b6..4e18d0bd7b 100644 --- a/example-wikipedia/pom.xml +++ b/example-wikipedia/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 io.github.openfeign diff --git a/fastjson2/pom.xml b/fastjson2/pom.xml index 7162b3f4a7..ac9f0cacb3 100644 --- a/fastjson2/pom.xml +++ b/fastjson2/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-fastjson2 diff --git a/feign-bom/pom.xml b/feign-bom/pom.xml index cc24c5f34c..51941b4a36 100644 --- a/feign-bom/pom.xml +++ b/feign-bom/pom.xml @@ -28,7 +28,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 ../pom.xml @@ -42,222 +42,222 @@ io.github.openfeign feign-core - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-gson - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-http-cache - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-jaxrs - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-httpclient - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-jaxrs2 - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-hc5 - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-hystrix - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-jackson - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-jackson3 - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-jackson-jaxb - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-jackson-jr - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-jaxb - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-jaxb-jakarta - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-jaxrs3 - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-jaxrs4 - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-java11 - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-jakarta - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-mock - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-json - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-okhttp - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-googlehttpclient - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-ribbon - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-sax - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-slf4j - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-spring - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-soap - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-soap-jakarta - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-reactive-wrappers - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-micrometer - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-dropwizard-metrics4 - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-dropwizard-metrics5 - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-kotlin - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-graphql - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-annotation-error-decoder - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-form - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-form-spring - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-moshi - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-fastjson2 - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-validation - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-validation-jakarta - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-vertx - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-vertx4-test - 13.13-SNAPSHOT + 13.13 io.github.openfeign feign-vertx5-test - 13.13-SNAPSHOT + 13.13 diff --git a/form-spring/pom.xml b/form-spring/pom.xml index 9d8cee4ad0..ce4bf7aaa4 100644 --- a/form-spring/pom.xml +++ b/form-spring/pom.xml @@ -23,7 +23,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-form-spring diff --git a/form/pom.xml b/form/pom.xml index 1777951c51..aad3e05cc5 100644 --- a/form/pom.xml +++ b/form/pom.xml @@ -23,7 +23,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-form diff --git a/googlehttpclient/pom.xml b/googlehttpclient/pom.xml index d89718f877..9f9997552b 100644 --- a/googlehttpclient/pom.xml +++ b/googlehttpclient/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-googlehttpclient diff --git a/graphql-apt/pom.xml b/graphql-apt/pom.xml index b0a60a58b8..d316c1a8bf 100644 --- a/graphql-apt/pom.xml +++ b/graphql-apt/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 io.github.openfeign.experimental diff --git a/graphql/pom.xml b/graphql/pom.xml index bd014420c1..b540524a3f 100644 --- a/graphql/pom.xml +++ b/graphql/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-graphql diff --git a/gson/pom.xml b/gson/pom.xml index dce2da755f..33bfafc1e1 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-gson diff --git a/hc5/pom.xml b/hc5/pom.xml index 22ab8b2d9e..83ec9ffd6f 100644 --- a/hc5/pom.xml +++ b/hc5/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-hc5 diff --git a/http-cache/pom.xml b/http-cache/pom.xml index 0722503a98..6388223cc4 100644 --- a/http-cache/pom.xml +++ b/http-cache/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-http-cache diff --git a/httpclient/pom.xml b/httpclient/pom.xml index b5ccf87a10..9c0cd57d19 100644 --- a/httpclient/pom.xml +++ b/httpclient/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-httpclient diff --git a/hystrix/pom.xml b/hystrix/pom.xml index f3c0589009..e1f005d877 100644 --- a/hystrix/pom.xml +++ b/hystrix/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-hystrix diff --git a/jackson-jaxb/pom.xml b/jackson-jaxb/pom.xml index 3e8dd5daef..a733366798 100644 --- a/jackson-jaxb/pom.xml +++ b/jackson-jaxb/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-jackson-jaxb diff --git a/jackson-jr/pom.xml b/jackson-jr/pom.xml index a1f5f2b3ad..bc1eb4011c 100644 --- a/jackson-jr/pom.xml +++ b/jackson-jr/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-jackson-jr diff --git a/jackson/pom.xml b/jackson/pom.xml index 3bed5201dd..c3f6520416 100644 --- a/jackson/pom.xml +++ b/jackson/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-jackson diff --git a/jackson3/pom.xml b/jackson3/pom.xml index 7a66128ed0..f00778e28f 100644 --- a/jackson3/pom.xml +++ b/jackson3/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-jackson3 diff --git a/jakarta/pom.xml b/jakarta/pom.xml index 0d992c5158..84505402be 100644 --- a/jakarta/pom.xml +++ b/jakarta/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-jakarta diff --git a/java11/pom.xml b/java11/pom.xml index a2dcc3ade6..2d21ae3713 100644 --- a/java11/pom.xml +++ b/java11/pom.xml @@ -21,7 +21,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-java11 diff --git a/jaxb-jakarta/pom.xml b/jaxb-jakarta/pom.xml index 9d9db921df..3e7a16d399 100644 --- a/jaxb-jakarta/pom.xml +++ b/jaxb-jakarta/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-jaxb-jakarta diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 7d8949d48f..b4a8b97de4 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-jaxb diff --git a/jaxrs/pom.xml b/jaxrs/pom.xml index 3f35a80f53..294bf526fc 100644 --- a/jaxrs/pom.xml +++ b/jaxrs/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-jaxrs diff --git a/jaxrs2/pom.xml b/jaxrs2/pom.xml index a1e3c801c5..561046981e 100644 --- a/jaxrs2/pom.xml +++ b/jaxrs2/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-jaxrs2 diff --git a/jaxrs3/pom.xml b/jaxrs3/pom.xml index 0e3193bb0d..6c1f41aca3 100644 --- a/jaxrs3/pom.xml +++ b/jaxrs3/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-jaxrs3 diff --git a/jaxrs4/pom.xml b/jaxrs4/pom.xml index 2b8f6defd1..2d2e4a3bb1 100644 --- a/jaxrs4/pom.xml +++ b/jaxrs4/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-jaxrs4 diff --git a/json/pom.xml b/json/pom.xml index 2e8e847a2d..169095489c 100644 --- a/json/pom.xml +++ b/json/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-json diff --git a/kotlin/pom.xml b/kotlin/pom.xml index a3d5857bc0..a78c53d6e5 100644 --- a/kotlin/pom.xml +++ b/kotlin/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-kotlin diff --git a/micrometer/pom.xml b/micrometer/pom.xml index c53fb2d6c8..bf5d2d2767 100644 --- a/micrometer/pom.xml +++ b/micrometer/pom.xml @@ -21,7 +21,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-micrometer Feign Micrometer diff --git a/mock/pom.xml b/mock/pom.xml index 97896b3c9b..cf767530f2 100644 --- a/mock/pom.xml +++ b/mock/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-mock diff --git a/moshi/pom.xml b/moshi/pom.xml index 81fb5c5c45..006e7ea606 100644 --- a/moshi/pom.xml +++ b/moshi/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-moshi diff --git a/okhttp/pom.xml b/okhttp/pom.xml index 30963523e1..a28dc7e54e 100644 --- a/okhttp/pom.xml +++ b/okhttp/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-okhttp diff --git a/pom.xml b/pom.xml index 5afb6b3601..cdb57878e0 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 pom Feign (Parent) diff --git a/reactive/pom.xml b/reactive/pom.xml index 9b6da88b97..65ba529b01 100644 --- a/reactive/pom.xml +++ b/reactive/pom.xml @@ -21,7 +21,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-reactive-wrappers diff --git a/ribbon/pom.xml b/ribbon/pom.xml index a4723450dc..aaeb03fd28 100644 --- a/ribbon/pom.xml +++ b/ribbon/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-ribbon diff --git a/sax/pom.xml b/sax/pom.xml index 071c4ace14..d83fb5b162 100644 --- a/sax/pom.xml +++ b/sax/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-sax diff --git a/slf4j/pom.xml b/slf4j/pom.xml index 54fb06d0a1..ae3fe95dbb 100644 --- a/slf4j/pom.xml +++ b/slf4j/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-slf4j diff --git a/soap-jakarta/pom.xml b/soap-jakarta/pom.xml index 99ba3ad6ad..377f6b4398 100644 --- a/soap-jakarta/pom.xml +++ b/soap-jakarta/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-soap-jakarta diff --git a/soap/pom.xml b/soap/pom.xml index 382a46ea81..1450e2ccba 100644 --- a/soap/pom.xml +++ b/soap/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-soap diff --git a/spring/pom.xml b/spring/pom.xml index 6c225e178b..06dd1e03d6 100644 --- a/spring/pom.xml +++ b/spring/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-spring diff --git a/spring4/pom.xml b/spring4/pom.xml index faf785491e..797c3fd78c 100644 --- a/spring4/pom.xml +++ b/spring4/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-spring4 diff --git a/validation-jakarta/pom.xml b/validation-jakarta/pom.xml index ee695e787f..a2fd4eb4f3 100644 --- a/validation-jakarta/pom.xml +++ b/validation-jakarta/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-validation-jakarta diff --git a/validation/pom.xml b/validation/pom.xml index adc5841f93..32e3f2bbb3 100644 --- a/validation/pom.xml +++ b/validation/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-validation diff --git a/vertx/feign-vertx/pom.xml b/vertx/feign-vertx/pom.xml index 518822a8b5..bef9ad2652 100644 --- a/vertx/feign-vertx/pom.xml +++ b/vertx/feign-vertx/pom.xml @@ -21,7 +21,7 @@ io.github.openfeign feign-vertx-parent - 13.13-SNAPSHOT + 13.13 feign-vertx diff --git a/vertx/feign-vertx4-test/pom.xml b/vertx/feign-vertx4-test/pom.xml index 8b48f808a7..cd6bfb8dd0 100644 --- a/vertx/feign-vertx4-test/pom.xml +++ b/vertx/feign-vertx4-test/pom.xml @@ -21,7 +21,7 @@ io.github.openfeign feign-vertx-parent - 13.13-SNAPSHOT + 13.13 feign-vertx4-test diff --git a/vertx/feign-vertx5-test/pom.xml b/vertx/feign-vertx5-test/pom.xml index 0501998954..efbf9b60ba 100644 --- a/vertx/feign-vertx5-test/pom.xml +++ b/vertx/feign-vertx5-test/pom.xml @@ -21,7 +21,7 @@ io.github.openfeign feign-vertx-parent - 13.13-SNAPSHOT + 13.13 feign-vertx5-test diff --git a/vertx/pom.xml b/vertx/pom.xml index 185047b3f1..c4c479ad47 100644 --- a/vertx/pom.xml +++ b/vertx/pom.xml @@ -21,7 +21,7 @@ io.github.openfeign feign-parent - 13.13-SNAPSHOT + 13.13 feign-vertx-parent From 46a38306e34e5709e5f7134f54864d77be1aaf94 Mon Sep 17 00:00:00 2001 From: velo Date: Sat, 13 Jun 2026 13:56:14 -0300 Subject: [PATCH 22/51] [ci skip] updating versions to next development iteration 13.14-SNAPSHOT --- annotation-error-decoder/pom.xml | 2 +- apt-test-generator/pom.xml | 2 +- benchmark/pom.xml | 2 +- core/pom.xml | 2 +- dropwizard-metrics4/pom.xml | 2 +- dropwizard-metrics5/pom.xml | 2 +- example-github-with-coroutine/pom.xml | 2 +- example-github/pom.xml | 2 +- example-wikipedia-with-springboot/pom.xml | 2 +- example-wikipedia/pom.xml | 2 +- fastjson2/pom.xml | 2 +- feign-bom/pom.xml | 90 +++++++++++------------ form-spring/pom.xml | 2 +- form/pom.xml | 2 +- googlehttpclient/pom.xml | 2 +- graphql-apt/pom.xml | 2 +- graphql/pom.xml | 2 +- gson/pom.xml | 2 +- hc5/pom.xml | 2 +- http-cache/pom.xml | 2 +- httpclient/pom.xml | 2 +- hystrix/pom.xml | 2 +- jackson-jaxb/pom.xml | 2 +- jackson-jr/pom.xml | 2 +- jackson/pom.xml | 2 +- jackson3/pom.xml | 2 +- jakarta/pom.xml | 2 +- java11/pom.xml | 2 +- jaxb-jakarta/pom.xml | 2 +- jaxb/pom.xml | 2 +- jaxrs/pom.xml | 2 +- jaxrs2/pom.xml | 2 +- jaxrs3/pom.xml | 2 +- jaxrs4/pom.xml | 2 +- json/pom.xml | 2 +- kotlin/pom.xml | 2 +- micrometer/pom.xml | 2 +- mock/pom.xml | 2 +- moshi/pom.xml | 2 +- okhttp/pom.xml | 2 +- pom.xml | 2 +- reactive/pom.xml | 2 +- ribbon/pom.xml | 2 +- sax/pom.xml | 2 +- slf4j/pom.xml | 2 +- soap-jakarta/pom.xml | 2 +- soap/pom.xml | 2 +- spring/pom.xml | 2 +- spring4/pom.xml | 2 +- validation-jakarta/pom.xml | 2 +- validation/pom.xml | 2 +- vertx/feign-vertx/pom.xml | 2 +- vertx/feign-vertx4-test/pom.xml | 2 +- vertx/feign-vertx5-test/pom.xml | 2 +- vertx/pom.xml | 2 +- 55 files changed, 99 insertions(+), 99 deletions(-) diff --git a/annotation-error-decoder/pom.xml b/annotation-error-decoder/pom.xml index 486fb8ab6f..f6ef0447f9 100644 --- a/annotation-error-decoder/pom.xml +++ b/annotation-error-decoder/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-annotation-error-decoder diff --git a/apt-test-generator/pom.xml b/apt-test-generator/pom.xml index 22786640b5..1617a5effb 100644 --- a/apt-test-generator/pom.xml +++ b/apt-test-generator/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT io.github.openfeign.experimental diff --git a/benchmark/pom.xml b/benchmark/pom.xml index 0ec4819be7..1114211299 100644 --- a/benchmark/pom.xml +++ b/benchmark/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-benchmark diff --git a/core/pom.xml b/core/pom.xml index c437c066d1..f33eed07af 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-core diff --git a/dropwizard-metrics4/pom.xml b/dropwizard-metrics4/pom.xml index fdad3983e7..834db1a745 100644 --- a/dropwizard-metrics4/pom.xml +++ b/dropwizard-metrics4/pom.xml @@ -21,7 +21,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-dropwizard-metrics4 Feign Dropwizard Metrics4 diff --git a/dropwizard-metrics5/pom.xml b/dropwizard-metrics5/pom.xml index ba23ee0790..03633fdff4 100644 --- a/dropwizard-metrics5/pom.xml +++ b/dropwizard-metrics5/pom.xml @@ -21,7 +21,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-dropwizard-metrics5 Feign Dropwizard Metrics5 diff --git a/example-github-with-coroutine/pom.xml b/example-github-with-coroutine/pom.xml index 186a1adee0..af9e97ccc8 100644 --- a/example-github-with-coroutine/pom.xml +++ b/example-github-with-coroutine/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-example-github-with-coroutine diff --git a/example-github/pom.xml b/example-github/pom.xml index 52f9bdd403..5f38c01dd9 100644 --- a/example-github/pom.xml +++ b/example-github/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-example-github diff --git a/example-wikipedia-with-springboot/pom.xml b/example-wikipedia-with-springboot/pom.xml index d93d37099c..066b63d52c 100644 --- a/example-wikipedia-with-springboot/pom.xml +++ b/example-wikipedia-with-springboot/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-example-wikipedia-with-springboot diff --git a/example-wikipedia/pom.xml b/example-wikipedia/pom.xml index 4e18d0bd7b..014bfdd0a3 100644 --- a/example-wikipedia/pom.xml +++ b/example-wikipedia/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT io.github.openfeign diff --git a/fastjson2/pom.xml b/fastjson2/pom.xml index ac9f0cacb3..8b1ae28bb0 100644 --- a/fastjson2/pom.xml +++ b/fastjson2/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-fastjson2 diff --git a/feign-bom/pom.xml b/feign-bom/pom.xml index 51941b4a36..6ba3f81610 100644 --- a/feign-bom/pom.xml +++ b/feign-bom/pom.xml @@ -28,7 +28,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT ../pom.xml @@ -42,222 +42,222 @@ io.github.openfeign feign-core - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-gson - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-http-cache - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-jaxrs - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-httpclient - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-jaxrs2 - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-hc5 - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-hystrix - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-jackson - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-jackson3 - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-jackson-jaxb - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-jackson-jr - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-jaxb - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-jaxb-jakarta - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-jaxrs3 - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-jaxrs4 - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-java11 - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-jakarta - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-mock - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-json - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-okhttp - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-googlehttpclient - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-ribbon - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-sax - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-slf4j - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-spring - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-soap - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-soap-jakarta - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-reactive-wrappers - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-micrometer - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-dropwizard-metrics4 - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-dropwizard-metrics5 - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-kotlin - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-graphql - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-annotation-error-decoder - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-form - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-form-spring - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-moshi - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-fastjson2 - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-validation - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-validation-jakarta - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-vertx - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-vertx4-test - 13.13 + 13.14-SNAPSHOT io.github.openfeign feign-vertx5-test - 13.13 + 13.14-SNAPSHOT diff --git a/form-spring/pom.xml b/form-spring/pom.xml index ce4bf7aaa4..cb08616583 100644 --- a/form-spring/pom.xml +++ b/form-spring/pom.xml @@ -23,7 +23,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-form-spring diff --git a/form/pom.xml b/form/pom.xml index aad3e05cc5..b248eecfb2 100644 --- a/form/pom.xml +++ b/form/pom.xml @@ -23,7 +23,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-form diff --git a/googlehttpclient/pom.xml b/googlehttpclient/pom.xml index 9f9997552b..c653ac1f31 100644 --- a/googlehttpclient/pom.xml +++ b/googlehttpclient/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-googlehttpclient diff --git a/graphql-apt/pom.xml b/graphql-apt/pom.xml index d316c1a8bf..10d645fa64 100644 --- a/graphql-apt/pom.xml +++ b/graphql-apt/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT io.github.openfeign.experimental diff --git a/graphql/pom.xml b/graphql/pom.xml index b540524a3f..9d931fcdd8 100644 --- a/graphql/pom.xml +++ b/graphql/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-graphql diff --git a/gson/pom.xml b/gson/pom.xml index 33bfafc1e1..eadea8b124 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-gson diff --git a/hc5/pom.xml b/hc5/pom.xml index 83ec9ffd6f..30a538f44a 100644 --- a/hc5/pom.xml +++ b/hc5/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-hc5 diff --git a/http-cache/pom.xml b/http-cache/pom.xml index 6388223cc4..c6ef73808f 100644 --- a/http-cache/pom.xml +++ b/http-cache/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-http-cache diff --git a/httpclient/pom.xml b/httpclient/pom.xml index 9c0cd57d19..a24f5671b7 100644 --- a/httpclient/pom.xml +++ b/httpclient/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-httpclient diff --git a/hystrix/pom.xml b/hystrix/pom.xml index e1f005d877..42cf19cf49 100644 --- a/hystrix/pom.xml +++ b/hystrix/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-hystrix diff --git a/jackson-jaxb/pom.xml b/jackson-jaxb/pom.xml index a733366798..5d0ab9493f 100644 --- a/jackson-jaxb/pom.xml +++ b/jackson-jaxb/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-jackson-jaxb diff --git a/jackson-jr/pom.xml b/jackson-jr/pom.xml index bc1eb4011c..33e15c76fc 100644 --- a/jackson-jr/pom.xml +++ b/jackson-jr/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-jackson-jr diff --git a/jackson/pom.xml b/jackson/pom.xml index c3f6520416..e5f58a13eb 100644 --- a/jackson/pom.xml +++ b/jackson/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-jackson diff --git a/jackson3/pom.xml b/jackson3/pom.xml index f00778e28f..f95ab2587f 100644 --- a/jackson3/pom.xml +++ b/jackson3/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-jackson3 diff --git a/jakarta/pom.xml b/jakarta/pom.xml index 84505402be..b026ef1a50 100644 --- a/jakarta/pom.xml +++ b/jakarta/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-jakarta diff --git a/java11/pom.xml b/java11/pom.xml index 2d21ae3713..15b342e11a 100644 --- a/java11/pom.xml +++ b/java11/pom.xml @@ -21,7 +21,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-java11 diff --git a/jaxb-jakarta/pom.xml b/jaxb-jakarta/pom.xml index 3e7a16d399..bfb3501e93 100644 --- a/jaxb-jakarta/pom.xml +++ b/jaxb-jakarta/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-jaxb-jakarta diff --git a/jaxb/pom.xml b/jaxb/pom.xml index b4a8b97de4..7e45e66109 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-jaxb diff --git a/jaxrs/pom.xml b/jaxrs/pom.xml index 294bf526fc..3647078c3e 100644 --- a/jaxrs/pom.xml +++ b/jaxrs/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-jaxrs diff --git a/jaxrs2/pom.xml b/jaxrs2/pom.xml index 561046981e..c583e9fe8f 100644 --- a/jaxrs2/pom.xml +++ b/jaxrs2/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-jaxrs2 diff --git a/jaxrs3/pom.xml b/jaxrs3/pom.xml index 6c1f41aca3..1d3960ab52 100644 --- a/jaxrs3/pom.xml +++ b/jaxrs3/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-jaxrs3 diff --git a/jaxrs4/pom.xml b/jaxrs4/pom.xml index 2d2e4a3bb1..397f4ea1a3 100644 --- a/jaxrs4/pom.xml +++ b/jaxrs4/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-jaxrs4 diff --git a/json/pom.xml b/json/pom.xml index 169095489c..759208abb6 100644 --- a/json/pom.xml +++ b/json/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-json diff --git a/kotlin/pom.xml b/kotlin/pom.xml index a78c53d6e5..8200b3135b 100644 --- a/kotlin/pom.xml +++ b/kotlin/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-kotlin diff --git a/micrometer/pom.xml b/micrometer/pom.xml index bf5d2d2767..e4f85282a6 100644 --- a/micrometer/pom.xml +++ b/micrometer/pom.xml @@ -21,7 +21,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-micrometer Feign Micrometer diff --git a/mock/pom.xml b/mock/pom.xml index cf767530f2..5fef7b967e 100644 --- a/mock/pom.xml +++ b/mock/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-mock diff --git a/moshi/pom.xml b/moshi/pom.xml index 006e7ea606..a46bc101bb 100644 --- a/moshi/pom.xml +++ b/moshi/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-moshi diff --git a/okhttp/pom.xml b/okhttp/pom.xml index a28dc7e54e..159ed7329d 100644 --- a/okhttp/pom.xml +++ b/okhttp/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-okhttp diff --git a/pom.xml b/pom.xml index cdb57878e0..5da5e97db3 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT pom Feign (Parent) diff --git a/reactive/pom.xml b/reactive/pom.xml index 65ba529b01..f22ef9272f 100644 --- a/reactive/pom.xml +++ b/reactive/pom.xml @@ -21,7 +21,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-reactive-wrappers diff --git a/ribbon/pom.xml b/ribbon/pom.xml index aaeb03fd28..7fb261e1ca 100644 --- a/ribbon/pom.xml +++ b/ribbon/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-ribbon diff --git a/sax/pom.xml b/sax/pom.xml index d83fb5b162..131ba32b80 100644 --- a/sax/pom.xml +++ b/sax/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-sax diff --git a/slf4j/pom.xml b/slf4j/pom.xml index ae3fe95dbb..3e5a8befb7 100644 --- a/slf4j/pom.xml +++ b/slf4j/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-slf4j diff --git a/soap-jakarta/pom.xml b/soap-jakarta/pom.xml index 377f6b4398..cfb055cf7b 100644 --- a/soap-jakarta/pom.xml +++ b/soap-jakarta/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-soap-jakarta diff --git a/soap/pom.xml b/soap/pom.xml index 1450e2ccba..2ea09d44c8 100644 --- a/soap/pom.xml +++ b/soap/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-soap diff --git a/spring/pom.xml b/spring/pom.xml index 06dd1e03d6..359db0a448 100644 --- a/spring/pom.xml +++ b/spring/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-spring diff --git a/spring4/pom.xml b/spring4/pom.xml index 797c3fd78c..27f7358c92 100644 --- a/spring4/pom.xml +++ b/spring4/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-spring4 diff --git a/validation-jakarta/pom.xml b/validation-jakarta/pom.xml index a2fd4eb4f3..91aec5f039 100644 --- a/validation-jakarta/pom.xml +++ b/validation-jakarta/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-validation-jakarta diff --git a/validation/pom.xml b/validation/pom.xml index 32e3f2bbb3..7aacc6e42f 100644 --- a/validation/pom.xml +++ b/validation/pom.xml @@ -22,7 +22,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-validation diff --git a/vertx/feign-vertx/pom.xml b/vertx/feign-vertx/pom.xml index bef9ad2652..5dc883e661 100644 --- a/vertx/feign-vertx/pom.xml +++ b/vertx/feign-vertx/pom.xml @@ -21,7 +21,7 @@ io.github.openfeign feign-vertx-parent - 13.13 + 13.14-SNAPSHOT feign-vertx diff --git a/vertx/feign-vertx4-test/pom.xml b/vertx/feign-vertx4-test/pom.xml index cd6bfb8dd0..f3467662a7 100644 --- a/vertx/feign-vertx4-test/pom.xml +++ b/vertx/feign-vertx4-test/pom.xml @@ -21,7 +21,7 @@ io.github.openfeign feign-vertx-parent - 13.13 + 13.14-SNAPSHOT feign-vertx4-test diff --git a/vertx/feign-vertx5-test/pom.xml b/vertx/feign-vertx5-test/pom.xml index efbf9b60ba..4f9d853346 100644 --- a/vertx/feign-vertx5-test/pom.xml +++ b/vertx/feign-vertx5-test/pom.xml @@ -21,7 +21,7 @@ io.github.openfeign feign-vertx-parent - 13.13 + 13.14-SNAPSHOT feign-vertx5-test diff --git a/vertx/pom.xml b/vertx/pom.xml index c4c479ad47..fbee5079b1 100644 --- a/vertx/pom.xml +++ b/vertx/pom.xml @@ -21,7 +21,7 @@ io.github.openfeign feign-parent - 13.13 + 13.14-SNAPSHOT feign-vertx-parent From f1e142673aa634d3af348ee0961107e726806119 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 05:14:52 +0000 Subject: [PATCH 23/51] build(deps): Bump org.hibernate.validator:hibernate-validator Bumps [org.hibernate.validator:hibernate-validator](https://github.com/hibernate/hibernate-validator) from 6.2.5.Final to 9.1.0.Final. - [Release notes](https://github.com/hibernate/hibernate-validator/releases) - [Changelog](https://github.com/hibernate/hibernate-validator/blob/main/changelog.txt) - [Commits](https://github.com/hibernate/hibernate-validator/compare/6.2.5.Final...9.1.0.Final) --- updated-dependencies: - dependency-name: org.hibernate.validator:hibernate-validator dependency-version: 9.1.0.Final dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5da5e97db3..86228b4199 100644 --- a/pom.xml +++ b/pom.xml @@ -237,7 +237,7 @@ 2.3.1 2.0.1.Final 3.1.1 - 6.2.5.Final + 9.1.0.Final 9.1.0.Final 3.0.4 6.0.0 From c99bc9b08e3f546e2b94b1d8bdb29337cf8afe5f Mon Sep 17 00:00:00 2001 From: Marvin Froeder Date: Mon, 15 Jun 2026 11:26:05 -0300 Subject: [PATCH 24/51] build(deps): import spring-boot-dependencies BOM to pin spring-boot artifacts to springboot.version Signed-off-by: Marvin Froeder --- pom.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pom.xml b/pom.xml index 66519298cd..7f02218d28 100644 --- a/pom.xml +++ b/pom.xml @@ -460,6 +460,14 @@ ${project.version} + + org.springframework.boot + spring-boot-dependencies + ${springboot.version} + pom + import + + org.junit junit-bom From e47fed56bd83bbd5f825005b3e91df2b48755594 Mon Sep 17 00:00:00 2001 From: Marvin Froeder Date: Mon, 15 Jun 2026 11:36:07 -0300 Subject: [PATCH 25/51] build(deps): scope spring-boot-dependencies BOM to form-spring to avoid disturbing other modules Signed-off-by: Marvin Froeder --- form-spring/pom.xml | 12 ++++++++++++ pom.xml | 8 -------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/form-spring/pom.xml b/form-spring/pom.xml index cb08616583..5561fb99fd 100644 --- a/form-spring/pom.xml +++ b/form-spring/pom.xml @@ -35,6 +35,18 @@ true + + + + org.springframework.boot + spring-boot-dependencies + ${springboot.version} + pom + import + + + + org.apache.commons diff --git a/pom.xml b/pom.xml index e8fb492279..84dc5f3e84 100644 --- a/pom.xml +++ b/pom.xml @@ -460,14 +460,6 @@ ${project.version} - - org.springframework.boot - spring-boot-dependencies - ${springboot.version} - pom - import - - org.junit junit-bom From 3dc2070bb74f160731522bd974fcb4a97ea7d65f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 05:12:51 +0000 Subject: [PATCH 26/51] build(deps-dev): Bump org.sonatype.central:central-publishing-maven-plugin Bumps [org.sonatype.central:central-publishing-maven-plugin](https://github.com/sonatype/central-publishing-maven-plugin) from 0.10.0 to 0.11.0. - [Commits](https://github.com/sonatype/central-publishing-maven-plugin/commits) --- updated-dependencies: - dependency-name: org.sonatype.central:central-publishing-maven-plugin dependency-version: 0.11.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 84dc5f3e84..82a0e40125 100644 --- a/pom.xml +++ b/pom.xml @@ -191,7 +191,7 @@ 3.3.1 6.0.2 0.1.1 - 0.10.0 + 0.11.0 3.5.6 0.300.0 file://${project.basedir}/src/config/bom.xml From 83f1896b70a7ab6b49a49c4f25d76909e343fc5d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 05:13:03 +0000 Subject: [PATCH 27/51] build(deps): Bump com.github.jknack:handlebars from 4.5.1 to 4.5.2 Bumps [com.github.jknack:handlebars](https://github.com/jknack/handlebars.java) from 4.5.1 to 4.5.2. - [Release notes](https://github.com/jknack/handlebars.java/releases) - [Commits](https://github.com/jknack/handlebars.java/compare/v4.5.1...v4.5.2) --- updated-dependencies: - dependency-name: com.github.jknack:handlebars dependency-version: 4.5.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 84dc5f3e84..652eb428be 100644 --- a/pom.xml +++ b/pom.xml @@ -220,7 +220,7 @@ 1.15.0 0.23.0 26.0 - 4.5.1 + 4.5.2 4.5.14 5.6.1 1.13.0 From 47e99c111c74ab9942db5f6bb4329610c27f6b3e Mon Sep 17 00:00:00 2001 From: velo Date: Wed, 17 Jun 2026 08:17:36 -0300 Subject: [PATCH 28/51] Keep javax validation on Hibernate Validator 6 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 86228b4199..5da5e97db3 100644 --- a/pom.xml +++ b/pom.xml @@ -237,7 +237,7 @@ 2.3.1 2.0.1.Final 3.1.1 - 9.1.0.Final + 6.2.5.Final 9.1.0.Final 3.0.4 6.0.0 From 39c4f41bac791fd7de77b0a751604d21e0886b85 Mon Sep 17 00:00:00 2001 From: seonwoojung Date: Wed, 17 Jun 2026 20:20:41 +0900 Subject: [PATCH 29/51] Honor jdk.httpclient.allowRestrictedHeaders in Http2Client (#2975) (#3419) The new Java HttpClient lets callers opt in to setting otherwise restricted headers (such as Host) via the jdk.httpclient.allowRestrictedHeaders system property. Http2Client kept its own static DISALLOWED_HEADERS_SET and stripped those headers before handing the request to the JDK client, so the opt-in had no effect and the header was silently dropped. Mirror jdk.internal.net.http.common.Utils#getDisallowedHeaders() by removing any headers listed in the property from the disallowed set. Signed-off-by: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> --- .../java/feign/http2client/Http2Client.java | 19 ++++++-- .../http2client/Http2ClientHeadersTest.java | 43 +++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 java11/src/test/java/feign/http2client/Http2ClientHeadersTest.java diff --git a/java11/src/main/java/feign/http2client/Http2Client.java b/java11/src/main/java/feign/http2client/Http2Client.java index 40d4548a22..8b59bfb310 100644 --- a/java11/src/main/java/feign/http2client/Http2Client.java +++ b/java11/src/main/java/feign/http2client/Http2Client.java @@ -242,13 +242,26 @@ private Builder newRequestBuilder(Request request, Options options) throws URISy * * @see jdk.internal.net.http.common.Utils.DISALLOWED_HEADERS_SET */ - private static final Set DISALLOWED_HEADERS_SET; + private static final Set DISALLOWED_HEADERS_SET = + disallowedHeaders(System.getProperty("jdk.httpclient.allowRestrictedHeaders")); - static { + /** + * Builds the set of headers the underlying JDK HttpClient refuses to send. Mirrors {@code + * jdk.internal.net.http.common.Utils#getDisallowedHeaders()}: headers listed (comma separated) in + * the {@code jdk.httpclient.allowRestrictedHeaders} system property are removed from the set, so + * that callers who opt in at the JDK level (e.g. to set {@code Host}) are not silently filtered + * out here as well. + */ + static Set disallowedHeaders(String allowRestrictedHeaders) { // A case insensitive TreeSet of strings. final TreeSet treeSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); treeSet.addAll(Set.of("connection", "content-length", "expect", "host", "upgrade")); - DISALLOWED_HEADERS_SET = Collections.unmodifiableSet(treeSet); + if (allowRestrictedHeaders != null) { + for (String header : allowRestrictedHeaders.split(",")) { + treeSet.remove(header.trim()); + } + } + return Collections.unmodifiableSet(treeSet); } private Map> filterRestrictedHeaders( diff --git a/java11/src/test/java/feign/http2client/Http2ClientHeadersTest.java b/java11/src/test/java/feign/http2client/Http2ClientHeadersTest.java new file mode 100644 index 0000000000..a185d033e8 --- /dev/null +++ b/java11/src/test/java/feign/http2client/Http2ClientHeadersTest.java @@ -0,0 +1,43 @@ +/* + * 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.http2client; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class Http2ClientHeadersTest { + + @Test + void filtersAllRestrictedHeadersByDefault() { + assertThat(Http2Client.disallowedHeaders(null)) + .contains("connection", "content-length", "expect", "host", "upgrade"); + } + + @Test + void allowsHeaderListedInSystemProperty() { + assertThat(Http2Client.disallowedHeaders("host")) + .doesNotContain("host") + .contains("connection", "content-length", "expect", "upgrade"); + } + + @Test + void allowsMultipleHeadersCaseInsensitivelyAndIgnoresWhitespace() { + assertThat(Http2Client.disallowedHeaders("Host, Connection")) + .doesNotContain("host", "connection") + .contains("content-length", "expect", "upgrade"); + } +} From 8694ed6a118dc038954aaa948e0a943aeceb8eb3 Mon Sep 17 00:00:00 2001 From: alhuda <50839256+alhudz@users.noreply.github.com> Date: Wed, 17 Jun 2026 16:50:52 +0530 Subject: [PATCH 30/51] escape crlf and quotes in multipart content-disposition headers (#3417) --- .../feign/form/multipart/AbstractWriter.java | 21 +++++++- .../form/multipart/SingleParameterWriter.java | 2 +- .../form/multipart/AbstractWriterTest.java | 49 +++++++++++++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 form/src/test/java/feign/form/multipart/AbstractWriterTest.java diff --git a/form/src/main/java/feign/form/multipart/AbstractWriter.java b/form/src/main/java/feign/form/multipart/AbstractWriter.java index 3062505197..d445974a36 100644 --- a/form/src/main/java/feign/form/multipart/AbstractWriter.java +++ b/form/src/main/java/feign/form/multipart/AbstractWriter.java @@ -64,10 +64,14 @@ protected void writeFileMetadata( val contentDespositionBuilder = new StringBuilder() .append("Content-Disposition: form-data; name=\"") - .append(name) + .append(escapeHeaderParameter(name)) .append("\""); if (fileName != null) { - contentDespositionBuilder.append("; ").append("filename=\"").append(fileName).append("\""); + contentDespositionBuilder + .append("; ") + .append("filename=\"") + .append(escapeHeaderParameter(fileName)) + .append("\""); } String fileContentType = contentType; @@ -94,4 +98,17 @@ protected void writeFileMetadata( output.write(string); } + + /** + * Escapes a {@code multipart/form-data} header parameter value so an attacker-supplied name or + * file name cannot break out of the quoted string and inject extra headers or part boundaries. + * Carriage return, line feed and double quote are percent-encoded, matching the WHATWG form-data + * encoding rules. + * + * @param value the raw parameter value. + * @return the escaped value, safe to place inside a quoted header parameter. + */ + protected static String escapeHeaderParameter(String value) { + return value.replace("\r", "%0D").replace("\n", "%0A").replace("\"", "%22"); + } } diff --git a/form/src/main/java/feign/form/multipart/SingleParameterWriter.java b/form/src/main/java/feign/form/multipart/SingleParameterWriter.java index d9c978cadf..c20c0ff1b6 100644 --- a/form/src/main/java/feign/form/multipart/SingleParameterWriter.java +++ b/form/src/main/java/feign/form/multipart/SingleParameterWriter.java @@ -37,7 +37,7 @@ protected void write(Output output, String key, Object value) throws EncodeExcep val string = new StringBuilder() .append("Content-Disposition: form-data; name=\"") - .append(key) + .append(escapeHeaderParameter(key)) .append('"') .append(CRLF) .append("Content-Type: text/plain; charset=") diff --git a/form/src/test/java/feign/form/multipart/AbstractWriterTest.java b/form/src/test/java/feign/form/multipart/AbstractWriterTest.java new file mode 100644 index 0000000000..05e40094cf --- /dev/null +++ b/form/src/test/java/feign/form/multipart/AbstractWriterTest.java @@ -0,0 +1,49 @@ +/* + * 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.form.multipart; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; + +import feign.form.FormData; +import org.junit.jupiter.api.Test; + +class AbstractWriterTest { + + @Test + void fileNameWithCrlfAndQuoteIsEscaped() { + Output output = new Output(UTF_8); + FormData formData = + new FormData("text/plain", "evil\"\r\nX-Injected: 1", "body".getBytes(UTF_8)); + + new FormDataWriter().write(output, "boundary", "file", formData); + String written = new String(output.toByteArray(), UTF_8); + + assertThat(written).contains("filename=\"evil%22%0D%0AX-Injected: 1\""); + assertThat(written).doesNotContain("\r\nX-Injected: 1"); + } + + @Test + void parameterNameWithCrlfIsEscaped() { + Output output = new Output(UTF_8); + + new SingleParameterWriter().write(output, "boundary", "a\"\r\nX-Injected: 1", "value"); + String written = new String(output.toByteArray(), UTF_8); + + assertThat(written).contains("name=\"a%22%0D%0AX-Injected: 1\""); + assertThat(written).doesNotContain("\r\nX-Injected: 1"); + } +} From 3f233b97fdbb7bac4fc6de5500096c29657670db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 05:12:22 +0000 Subject: [PATCH 31/51] build(deps): Bump org.openrewrite.recipe:rewrite-testing-frameworks Bumps [org.openrewrite.recipe:rewrite-testing-frameworks](https://github.com/openrewrite/rewrite-testing-frameworks) from 3.37.0 to 3.38.0. - [Release notes](https://github.com/openrewrite/rewrite-testing-frameworks/releases) - [Commits](https://github.com/openrewrite/rewrite-testing-frameworks/compare/v3.37.0...v3.38.0) --- updated-dependencies: - dependency-name: org.openrewrite.recipe:rewrite-testing-frameworks dependency-version: 3.38.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5465e9161a..d71320b1af 100644 --- a/pom.xml +++ b/pom.xml @@ -206,7 +206,7 @@ 1.2.8 4.0.0 6.41.0 - 3.37.0 + 3.38.0 3.36.0 0.26.1 1.0 From 1a47cede2fd445a37f34a4698c5b6e680baa48af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 05:13:22 +0000 Subject: [PATCH 32/51] build(deps): Bump org.hibernate.validator:hibernate-validator Bumps [org.hibernate.validator:hibernate-validator](https://github.com/hibernate/hibernate-validator) from 6.2.5.Final to 9.1.0.Final. - [Release notes](https://github.com/hibernate/hibernate-validator/releases) - [Changelog](https://github.com/hibernate/hibernate-validator/blob/main/changelog.txt) - [Commits](https://github.com/hibernate/hibernate-validator/compare/6.2.5.Final...9.1.0.Final) --- updated-dependencies: - dependency-name: org.hibernate.validator:hibernate-validator dependency-version: 9.1.0.Final dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5465e9161a..d8e60d39f4 100644 --- a/pom.xml +++ b/pom.xml @@ -237,7 +237,7 @@ 2.3.1 2.0.1.Final 3.1.1 - 6.2.5.Final + 9.1.0.Final 9.1.0.Final 3.0.4 6.0.0 From a9bf809cddfb55430af58bcfa04475e9c0cf4165 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 05:24:30 +0000 Subject: [PATCH 33/51] build(deps-dev): Bump org.openrewrite.maven:rewrite-maven-plugin Bumps [org.openrewrite.maven:rewrite-maven-plugin](https://github.com/openrewrite/rewrite-maven-plugin) from 6.41.0 to 6.42.0. - [Release notes](https://github.com/openrewrite/rewrite-maven-plugin/releases) - [Commits](https://github.com/openrewrite/rewrite-maven-plugin/compare/v6.41.0...v6.42.0) --- updated-dependencies: - dependency-name: org.openrewrite.maven:rewrite-maven-plugin dependency-version: 6.42.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d71320b1af..1684a52724 100644 --- a/pom.xml +++ b/pom.xml @@ -205,7 +205,7 @@ 3.2.0 1.2.8 4.0.0 - 6.41.0 + 6.42.0 3.38.0 3.36.0 0.26.1 From dc0525d51dbc4a30695c8bd150510c38bd963584 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 05:24:51 +0000 Subject: [PATCH 34/51] build(deps): Bump org.openrewrite.recipe:rewrite-migrate-java Bumps [org.openrewrite.recipe:rewrite-migrate-java](https://github.com/openrewrite/rewrite-migrate-java) from 3.36.0 to 3.37.0. - [Release notes](https://github.com/openrewrite/rewrite-migrate-java/releases) - [Commits](https://github.com/openrewrite/rewrite-migrate-java/compare/v3.36.0...v3.37.0) --- updated-dependencies: - dependency-name: org.openrewrite.recipe:rewrite-migrate-java dependency-version: 3.37.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d71320b1af..b1181d6f0c 100644 --- a/pom.xml +++ b/pom.xml @@ -207,7 +207,7 @@ 4.0.0 6.41.0 3.38.0 - 3.36.0 + 3.37.0 0.26.1 1.0 From 35609a1bda8a30232e046001c334fb6b7416970c Mon Sep 17 00:00:00 2001 From: velo Date: Thu, 18 Jun 2026 08:17:17 -0300 Subject: [PATCH 35/51] Keep javax validation on compatible provider --- pom.xml | 4 ++-- validation-jakarta/pom.xml | 2 +- validation/pom.xml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index d8e60d39f4..15c9e17d7c 100644 --- a/pom.xml +++ b/pom.xml @@ -237,8 +237,8 @@ 2.3.1 2.0.1.Final 3.1.1 - 9.1.0.Final - 9.1.0.Final + 6.2.5.Final + 9.1.0.Final 3.0.4 6.0.0 1.19.4 diff --git a/validation-jakarta/pom.xml b/validation-jakarta/pom.xml index 91aec5f039..2a34260bcf 100644 --- a/validation-jakarta/pom.xml +++ b/validation-jakarta/pom.xml @@ -58,7 +58,7 @@ org.hibernate.validator hibernate-validator - ${hibernate-validator-8.version} + ${hibernate-validator-jakarta.version} test diff --git a/validation/pom.xml b/validation/pom.xml index 7aacc6e42f..65ca7898b9 100644 --- a/validation/pom.xml +++ b/validation/pom.xml @@ -52,7 +52,7 @@ org.hibernate.validator hibernate-validator - ${hibernate-validator-6.version} + ${hibernate-validator-javax.version} test From db9ae4b451448dd9cc3ef3bdd150feff1ff81e40 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 05:13:02 +0000 Subject: [PATCH 36/51] build(deps): Bump org.hibernate.validator:hibernate-validator Bumps [org.hibernate.validator:hibernate-validator](https://github.com/hibernate/hibernate-validator) from 6.2.5.Final to 9.1.0.Final. - [Release notes](https://github.com/hibernate/hibernate-validator/releases) - [Changelog](https://github.com/hibernate/hibernate-validator/blob/main/changelog.txt) - [Commits](https://github.com/hibernate/hibernate-validator/compare/6.2.5.Final...9.1.0.Final) --- updated-dependencies: - dependency-name: org.hibernate.validator:hibernate-validator dependency-version: 9.1.0.Final dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3fd3304d2f..d6cd1421c0 100644 --- a/pom.xml +++ b/pom.xml @@ -237,7 +237,7 @@ 2.3.1 2.0.1.Final 3.1.1 - 6.2.5.Final + 9.1.0.Final 9.1.0.Final 3.0.4 6.0.0 From 9d7ffef247f1421947e31c6d08f40b76935cf886 Mon Sep 17 00:00:00 2001 From: velo Date: Fri, 19 Jun 2026 08:16:02 -0300 Subject: [PATCH 37/51] Keep javax validation on Hibernate Validator 6 --- .github/dependabot.yml | 3 +++ pom.xml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fa14f951f4..d25a05febb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -24,6 +24,9 @@ updates: # SAAJ impl at 1.x (soap) and 3.x (soap-jakarta) - dependency-name: "com.sun.xml.messaging.saaj:saaj-impl" update-types: ["version-update:semver-major"] + # feign-validation is intentionally on javax.validation; Hibernate Validator 7+ is Jakarta-only. + - dependency-name: "org.hibernate.validator:hibernate-validator" + update-types: ["version-update:semver-major"] # Jersey 2.x for jaxrs2 - package-ecosystem: "maven" diff --git a/pom.xml b/pom.xml index d6cd1421c0..3fd3304d2f 100644 --- a/pom.xml +++ b/pom.xml @@ -237,7 +237,7 @@ 2.3.1 2.0.1.Final 3.1.1 - 9.1.0.Final + 6.2.5.Final 9.1.0.Final 3.0.4 6.0.0 From 247fb46def599ec82e29dd5332bf53600a65ed0e Mon Sep 17 00:00:00 2001 From: alhuda <50839256+alhudz@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:48:56 +0530 Subject: [PATCH 38/51] fall back to utf-8 for invalid content-type charset (#3427) Response.charset() and feign-form's FormEncoder.getCharset() passed the parsed charset token straight to Charset.forName, so an unsupported or illegal name from a Content-Type header threw instead of using the documented UTF-8 default. Catch the charset exceptions in both and fall back to UTF-8, as FeignException.getResponseCharset already does. --- core/src/main/java/feign/Response.java | 8 ++- core/src/test/java/feign/ResponseTest.java | 30 +++++++++++ .../src/main/java/feign/form/FormEncoder.java | 11 +++- .../feign/form/FormEncoderCharsetTest.java | 53 +++++++++++++++++++ 4 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 form/src/test/java/feign/form/FormEncoderCharsetTest.java diff --git a/core/src/main/java/feign/Response.java b/core/src/main/java/feign/Response.java index 2bf64d56e2..c1b175ced8 100644 --- a/core/src/main/java/feign/Response.java +++ b/core/src/main/java/feign/Response.java @@ -20,7 +20,9 @@ import feign.Request.ProtocolVersion; import java.io.*; import java.nio.charset.Charset; +import java.nio.charset.IllegalCharsetNameException; import java.nio.charset.StandardCharsets; +import java.nio.charset.UnsupportedCharsetException; import java.util.*; /** An immutable response to an http invocation which only returns string content. */ @@ -219,7 +221,11 @@ public Charset charset() { String[] charsetParts = contentTypeParmeters[1].split("="); if (charsetParts.length == 2 && "charset".equalsIgnoreCase(charsetParts[0].trim())) { String charsetString = charsetParts[1].replaceAll("\"", ""); - return Charset.forName(charsetString); + try { + return Charset.forName(charsetString); + } catch (IllegalCharsetNameException | UnsupportedCharsetException e) { + return Util.UTF_8; + } } } } diff --git a/core/src/test/java/feign/ResponseTest.java b/core/src/test/java/feign/ResponseTest.java index 9af1ceb887..e781bce669 100644 --- a/core/src/test/java/feign/ResponseTest.java +++ b/core/src/test/java/feign/ResponseTest.java @@ -87,6 +87,36 @@ void charsetSupportsMediaTypesWithQuotedCharset() { assertThat(response.charset()).isEqualTo(Util.UTF_8); } + @Test + void charsetDefaultsToUtf8ForIllegalCharsetName() { + Map> headersMap = new LinkedHashMap<>(); + headersMap.put("Content-Type", Collections.singletonList("text/plain; charset=@")); + Response response = + Response.builder() + .status(200) + .headers(headersMap) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .body(new byte[0]) + .build(); + assertThat(response.charset()).isEqualTo(Util.UTF_8); + } + + @Test + void charsetDefaultsToUtf8ForUnsupportedCharset() { + Map> headersMap = new LinkedHashMap<>(); + headersMap.put("Content-Type", Collections.singletonList("text/plain; charset=made-up-99")); + Response response = + Response.builder() + .status(200) + .headers(headersMap) + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .body(new byte[0]) + .build(); + assertThat(response.charset()).isEqualTo(Util.UTF_8); + } + @Test void headerValuesWithSameNameOnlyVaryingInCaseAreMerged() { Map> headersMap = new LinkedHashMap<>(); diff --git a/form/src/main/java/feign/form/FormEncoder.java b/form/src/main/java/feign/form/FormEncoder.java index e01e24976c..fb05cde7cd 100644 --- a/form/src/main/java/feign/form/FormEncoder.java +++ b/form/src/main/java/feign/form/FormEncoder.java @@ -27,6 +27,8 @@ import feign.codec.Encoder; import java.lang.reflect.Type; import java.nio.charset.Charset; +import java.nio.charset.IllegalCharsetNameException; +import java.nio.charset.UnsupportedCharsetException; import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -130,6 +132,13 @@ private String getContentTypeValue(Map> headers) { private Charset getCharset(String contentTypeValue) { val matcher = CHARSET_PATTERN.matcher(contentTypeValue); - return matcher.find() ? Charset.forName(matcher.group(1)) : UTF_8; + if (!matcher.find()) { + return UTF_8; + } + try { + return Charset.forName(matcher.group(1)); + } catch (IllegalCharsetNameException | UnsupportedCharsetException e) { + return UTF_8; + } } } diff --git a/form/src/test/java/feign/form/FormEncoderCharsetTest.java b/form/src/test/java/feign/form/FormEncoderCharsetTest.java new file mode 100644 index 0000000000..d100fa65bc --- /dev/null +++ b/form/src/test/java/feign/form/FormEncoderCharsetTest.java @@ -0,0 +1,53 @@ +/* + * 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.form; + +import static org.assertj.core.api.Assertions.assertThat; + +import feign.RequestTemplate; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class FormEncoderCharsetTest { + + @Test + void illegalCharsetInContentTypeFallsBackToUtf8() { + RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "application/x-www-form-urlencoded; charset=_bad"); + + Map data = new LinkedHashMap<>(); + data.put("foo", "bar"); + + new FormEncoder().encode(data, Map.class, template); + + assertThat(new String(template.body(), StandardCharsets.UTF_8)).isEqualTo("foo=bar"); + } + + @Test + void unsupportedCharsetInContentTypeFallsBackToUtf8() { + RequestTemplate template = new RequestTemplate(); + template.header("Content-Type", "application/x-www-form-urlencoded; charset=made-up-99"); + + Map data = new LinkedHashMap<>(); + data.put("foo", "bar"); + + new FormEncoder().encode(data, Map.class, template); + + assertThat(new String(template.body(), StandardCharsets.UTF_8)).isEqualTo("foo=bar"); + } +} From 837dcd575f0840750b6683db413c0a06a4cc7268 Mon Sep 17 00:00:00 2001 From: seonwoojung Date: Fri, 19 Jun 2026 20:19:29 +0900 Subject: [PATCH 39/51] Make Expressions max length configurable (#3422) The maximum length of a single template expression was hard-coded to 10000 characters in feign.template.Expressions, throwing IllegalArgumentException for anything longer with no way to opt out. Make the limit configurable through the "feign.template.expression.maxLength" system property (default 10000), and allow disabling the check entirely by setting it to a non-positive value. Fixes #2916 Signed-off-by: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> --- .../main/java/feign/template/Expressions.java | 21 ++++++++-- .../java/feign/template/ExpressionsTest.java | 40 +++++++++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/feign/template/Expressions.java b/core/src/main/java/feign/template/Expressions.java index 65fa5123e2..1368364d59 100644 --- a/core/src/main/java/feign/template/Expressions.java +++ b/core/src/main/java/feign/template/Expressions.java @@ -25,7 +25,14 @@ public final class Expressions { - private static final int MAX_EXPRESSION_LENGTH = 10000; + /** + * System property controlling the maximum allowed length of a single expression. Defaults to + * {@link #DEFAULT_MAX_EXPRESSION_LENGTH}. Setting it to {@code 0} (or any non-positive value) + * disables the length check entirely. + */ + static final String MAX_EXPRESSION_LENGTH_PROPERTY = "feign.template.expression.maxLength"; + + private static final int DEFAULT_MAX_EXPRESSION_LENGTH = 10000; private static final String PATH_STYLE_OPERATOR = ";"; @@ -73,10 +80,16 @@ public static Expression create(final String value) { throw new IllegalArgumentException("an expression is required."); } - /* Check if the expression is too long */ - if (expression.length() > MAX_EXPRESSION_LENGTH) { + /* + * Check if the expression is too long. The limit is configurable through the + * "feign.template.expression.maxLength" system property and can be disabled by setting it to a + * non-positive value. + */ + final int maxExpressionLength = + Integer.getInteger(MAX_EXPRESSION_LENGTH_PROPERTY, DEFAULT_MAX_EXPRESSION_LENGTH); + if (maxExpressionLength > 0 && expression.length() > maxExpressionLength) { throw new IllegalArgumentException( - "expression is too long. Max length: " + MAX_EXPRESSION_LENGTH); + "expression is too long. Max length: " + maxExpressionLength); } /* create a new regular expression matcher for the expression */ diff --git a/core/src/test/java/feign/template/ExpressionsTest.java b/core/src/test/java/feign/template/ExpressionsTest.java index 0586bd231e..77241770f5 100644 --- a/core/src/test/java/feign/template/ExpressionsTest.java +++ b/core/src/test/java/feign/template/ExpressionsTest.java @@ -16,13 +16,53 @@ package feign.template; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatObject; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.Collections; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; class ExpressionsTest { + @AfterEach + void clearMaxExpressionLengthProperty() { + System.clearProperty(Expressions.MAX_EXPRESSION_LENGTH_PROPERTY); + } + + @Test + void tooLongExpressionFailsWithDefaultLimit() { + String tooLong = "{" + "a".repeat(10001) + "}"; + assertThatThrownBy(() -> Expressions.create(tooLong)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("expression is too long"); + } + + @Test + void maxExpressionLengthIsConfigurable() { + System.setProperty(Expressions.MAX_EXPRESSION_LENGTH_PROPERTY, "5"); + assertThatThrownBy(() -> Expressions.create("{foobar}")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Max length: 5"); + } + + @Test + void lengthCheckCanBeDisabled() { + // An expression well beyond the default 10000 limit, expressed as a name plus a regular + // expression value modifier so the disabled length check is exercised in isolation. + String longExpression = "{name:" + "a".repeat(15000) + "}"; + assertThatThrownBy(() -> Expressions.create(longExpression)) + .as("guarded by default limit") + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("expression is too long"); + + System.setProperty(Expressions.MAX_EXPRESSION_LENGTH_PROPERTY, "0"); + assertThatNoException() + .as("length check disabled") + .isThrownBy(() -> Expressions.create(longExpression)); + } + @Test void simpleExpression() { Expression expression = Expressions.create("{foo}"); From bd7a5377fdf1e6519e072402d0f7dc751514c663 Mon Sep 17 00:00:00 2001 From: seonwoojung Date: Fri, 19 Jun 2026 20:20:01 +0900 Subject: [PATCH 40/51] Preserve delegate content type for multipart parameters (#3382) `DelegateWriter` encodes a parameter through the delegate `Encoder` (e.g. the Jackson encoder), which sets the correct `Content-Type` (such as `application/json`) on the throwaway `RequestTemplate`. That header was discarded and `SingleParameterWriter` hard-coded `text/plain`, so JSON parts of a multipart request were emitted as `text/plain`. `DelegateWriter` now reads the `Content-Type` produced by the delegate and passes it through to `SingleParameterWriter.writeWithContentType`, falling back to `text/plain; charset=` when the delegate sets no content type. The previous behaviour for plain single parameters is unchanged. Fixes #2813 Signed-off-by: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) --- .../feign/form/multipart/DelegateWriter.java | 14 ++++- .../form/multipart/SingleParameterWriter.java | 21 ++++++- .../form/multipart/DelegateWriterTest.java | 57 +++++++++++++++++++ 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 form/src/test/java/feign/form/multipart/DelegateWriterTest.java diff --git a/form/src/main/java/feign/form/multipart/DelegateWriter.java b/form/src/main/java/feign/form/multipart/DelegateWriter.java index 7161c21a46..17620d77f6 100644 --- a/form/src/main/java/feign/form/multipart/DelegateWriter.java +++ b/form/src/main/java/feign/form/multipart/DelegateWriter.java @@ -48,6 +48,18 @@ protected void write(Output output, String key, Object value) throws EncodeExcep delegate.encode(value, value.getClass(), fake); val bytes = fake.body(); val string = new String(bytes, output.getCharset()).replaceAll("\n", ""); - parameterWriter.write(output, key, string); + parameterWriter.writeWithContentType(output, key, string, contentType(fake)); + } + + private static String contentType(RequestTemplate template) { + val headers = template.headers().get("Content-Type"); + if (headers != null) { + for (val header : headers) { + if (header != null && !header.isEmpty()) { + return header; + } + } + } + return null; } } diff --git a/form/src/main/java/feign/form/multipart/SingleParameterWriter.java b/form/src/main/java/feign/form/multipart/SingleParameterWriter.java index c20c0ff1b6..55eab20204 100644 --- a/form/src/main/java/feign/form/multipart/SingleParameterWriter.java +++ b/form/src/main/java/feign/form/multipart/SingleParameterWriter.java @@ -34,14 +34,31 @@ public boolean isApplicable(Object value) { @Override protected void write(Output output, String key, Object value) throws EncodeException { + writeWithContentType(output, key, value, null); + } + + /** + * Writes a single parameter using the given content type. + * + * @param output output writer. + * @param key name for piece of data. + * @param value piece of data. + * @param contentType the content type of the part. May be {@code null}, in which case {@code + * text/plain} with the output charset is used. + * @throws EncodeException in case of write errors + */ + protected void writeWithContentType(Output output, String key, Object value, String contentType) + throws EncodeException { + val contentTypeHeader = + contentType != null ? contentType : "text/plain; charset=" + output.getCharset().name(); val string = new StringBuilder() .append("Content-Disposition: form-data; name=\"") .append(escapeHeaderParameter(key)) .append('"') .append(CRLF) - .append("Content-Type: text/plain; charset=") - .append(output.getCharset().name()) + .append("Content-Type: ") + .append(contentTypeHeader) .append(CRLF) .append(CRLF) .append(value.toString()) diff --git a/form/src/test/java/feign/form/multipart/DelegateWriterTest.java b/form/src/test/java/feign/form/multipart/DelegateWriterTest.java new file mode 100644 index 0000000000..365cfe9d0e --- /dev/null +++ b/form/src/test/java/feign/form/multipart/DelegateWriterTest.java @@ -0,0 +1,57 @@ +/* + * 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.form.multipart; + +import static org.assertj.core.api.Assertions.assertThat; + +import feign.codec.Encoder; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +class DelegateWriterTest { + + private static final String BOUNDARY = "boundary"; + + private static final String KEY = "metadata"; + + @Test + void usesContentTypeFromDelegate() throws Exception { + Encoder delegate = + (object, bodyType, template) -> { + template.header("Content-Type", "application/json"); + template.body("{\"hash\":\"somehash\"}"); + }; + + assertThat(write(delegate)) + .contains("Content-Type: application/json") + .doesNotContain("Content-Type: text/plain"); + } + + @Test + void fallsBackToTextPlainWhenDelegateSetsNoContentType() throws Exception { + Encoder delegate = (object, bodyType, template) -> template.body("plain"); + + assertThat(write(delegate)).contains("Content-Type: text/plain; charset=UTF-8"); + } + + private static String write(Encoder delegate) throws Exception { + DelegateWriter writer = new DelegateWriter(delegate); + try (Output output = new Output(StandardCharsets.UTF_8)) { + writer.write(output, BOUNDARY, KEY, new Object()); + return new String(output.toByteArray(), StandardCharsets.UTF_8); + } + } +} From 425cc8177c31c5cb4375d49f6c11e46e1cc6e8a0 Mon Sep 17 00:00:00 2001 From: alhuda <50839256+alhudz@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:45:46 +0530 Subject: [PATCH 41/51] cap Content-Length before narrowing to int in Http2Client (#3431) --- .../java/feign/http2client/Http2Client.java | 6 +- .../Http2ClientContentLengthTest.java | 113 ++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 java11/src/test/java/feign/http2client/Http2ClientContentLengthTest.java diff --git a/java11/src/main/java/feign/http2client/Http2Client.java b/java11/src/main/java/feign/http2client/Http2Client.java index 8b59bfb310..9eb718df97 100644 --- a/java11/src/main/java/feign/http2client/Http2Client.java +++ b/java11/src/main/java/feign/http2client/Http2Client.java @@ -130,6 +130,10 @@ public CompletableFuture execute( protected Response toFeignResponse(Request request, HttpResponse httpResponse) { final OptionalLong length = httpResponse.headers().firstValueAsLong("Content-Length"); + final Integer contentLength = + length.isPresent() && length.getAsLong() >= 0 && length.getAsLong() <= Integer.MAX_VALUE + ? (int) length.getAsLong() + : null; InputStream body = httpResponse.body(); @@ -144,7 +148,7 @@ protected Response toFeignResponse(Request request, HttpResponse ht return Response.builder() .protocolVersion(enumForName(ProtocolVersion.class, httpResponse.version())) - .body(body, length.isPresent() ? (int) length.getAsLong() : null) + .body(body, contentLength) .reason(httpResponse.headers().firstValue("Reason-Phrase").orElse(null)) .request(request) .status(httpResponse.statusCode()) diff --git a/java11/src/test/java/feign/http2client/Http2ClientContentLengthTest.java b/java11/src/test/java/feign/http2client/Http2ClientContentLengthTest.java new file mode 100644 index 0000000000..ff6ed83fa5 --- /dev/null +++ b/java11/src/test/java/feign/http2client/Http2ClientContentLengthTest.java @@ -0,0 +1,113 @@ +/* + * 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.http2client; + +import static org.assertj.core.api.Assertions.assertThat; + +import feign.Request; +import feign.Request.HttpMethod; +import feign.Response; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.URI; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.net.ssl.SSLSession; +import org.junit.jupiter.api.Test; + +class Http2ClientContentLengthTest { + + private static HttpResponse responseWithContentLength(String contentLength) { + final HttpHeaders headers = + HttpHeaders.of(Map.of("Content-Length", List.of(contentLength)), (name, value) -> true); + return new HttpResponse<>() { + @Override + public int statusCode() { + return 200; + } + + @Override + public HttpRequest request() { + return null; + } + + @Override + public Optional> previousResponse() { + return Optional.empty(); + } + + @Override + public HttpHeaders headers() { + return headers; + } + + @Override + public InputStream body() { + return new ByteArrayInputStream(new byte[0]); + } + + @Override + public Optional sslSession() { + return Optional.empty(); + } + + @Override + public URI uri() { + return URI.create("http://localhost"); + } + + @Override + public Version version() { + return Version.HTTP_2; + } + }; + } + + private static Response decode(String contentLength) { + final Request request = + Request.create( + HttpMethod.GET, + "http://localhost", + Collections.emptyMap(), + null, + StandardCharsets.UTF_8, + null); + return new Http2Client().toFeignResponse(request, responseWithContentLength(contentLength)); + } + + @Test + void contentLengthAboveIntMaxIsReportedAsUnknown() { + // 2^31, a valid Content-Length larger than Integer.MAX_VALUE + assertThat(decode("2147483648").body().length()).isNull(); + } + + @Test + void negativeContentLengthIsReportedAsUnknown() { + assertThat(decode("-1").body().length()).isNull(); + } + + @Test + void contentLengthWithinIntRangeIsPreserved() { + assertThat(decode("1024").body().length()).isEqualTo(1024); + } +} From 0a250fd90a37ec87085a4d59ecb94ceecd9c3d8a Mon Sep 17 00:00:00 2001 From: alhuda <50839256+alhudz@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:46:02 +0530 Subject: [PATCH 42/51] generate multipart boundary from SecureRandom (#3429) --- .../form/MultipartFormContentProcessor.java | 16 ++++- .../feign/form/MultipartBoundaryTest.java | 59 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 form/src/test/java/feign/form/MultipartBoundaryTest.java diff --git a/form/src/main/java/feign/form/MultipartFormContentProcessor.java b/form/src/main/java/feign/form/MultipartFormContentProcessor.java index af31d36eb6..2a38dc6d6b 100644 --- a/form/src/main/java/feign/form/MultipartFormContentProcessor.java +++ b/form/src/main/java/feign/form/MultipartFormContentProcessor.java @@ -33,6 +33,7 @@ import feign.form.multipart.Writer; import java.io.IOException; import java.nio.charset.Charset; +import java.security.SecureRandom; import java.util.Collection; import java.util.Collections; import java.util.Deque; @@ -49,6 +50,8 @@ @FieldDefaults(level = PRIVATE, makeFinal = true) public class MultipartFormContentProcessor implements ContentProcessor { + private static final SecureRandom RANDOM = new SecureRandom(); + Deque writers; Writer defaultPerocessor; @@ -75,7 +78,7 @@ public MultipartFormContentProcessor(Encoder delegate) { @Override public void process(RequestTemplate template, Charset charset, Map data) throws EncodeException { - val boundary = Long.toHexString(System.currentTimeMillis()); + val boundary = randomBoundary(); try (val output = new Output(charset)) { for (val entry : data.entrySet()) { if (entry == null || entry.getKey() == null || entry.getValue() == null) { @@ -158,4 +161,15 @@ private Writer findApplicableWriter(Object value) { } return defaultPerocessor; } + + private static String randomBoundary() { + val token = new byte[16]; + RANDOM.nextBytes(token); + val builder = new StringBuilder(token.length * 2); + for (val octet : token) { + builder.append(Character.forDigit((octet >> 4) & 0xF, 16)); + builder.append(Character.forDigit(octet & 0xF, 16)); + } + return builder.toString(); + } } diff --git a/form/src/test/java/feign/form/MultipartBoundaryTest.java b/form/src/test/java/feign/form/MultipartBoundaryTest.java new file mode 100644 index 0000000000..26399fcdc6 --- /dev/null +++ b/form/src/test/java/feign/form/MultipartBoundaryTest.java @@ -0,0 +1,59 @@ +/* + * 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.form; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; + +import feign.RequestTemplate; +import feign.codec.Encoder; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class MultipartBoundaryTest { + + private static final Encoder NOOP_DELEGATE = (object, bodyType, template) -> {}; + + @Test + void boundaryIsNotDerivedFromTheClock() { + long before = System.currentTimeMillis(); + String boundary = generateBoundary(); + long after = System.currentTimeMillis(); + + for (long millis = before; millis <= after; millis++) { + assertThat(boundary).isNotEqualTo(Long.toHexString(millis)); + } + } + + @Test + void boundaryDiffersBetweenRequests() { + assertThat(generateBoundary()).isNotEqualTo(generateBoundary()); + } + + private static String generateBoundary() { + MultipartFormContentProcessor processor = new MultipartFormContentProcessor(NOOP_DELEGATE); + RequestTemplate template = new RequestTemplate(); + + Map data = new LinkedHashMap<>(); + data.put("field", "value"); + processor.process(template, UTF_8, data); + + String contentType = template.headers().get("Content-Type").iterator().next(); + int index = contentType.indexOf("boundary="); + return contentType.substring(index + "boundary=".length()); + } +} From 265c6570d90170371c7b42ec0d788519678bf98b Mon Sep 17 00:00:00 2001 From: alhuda <50839256+alhudz@users.noreply.github.com> Date: Sun, 21 Jun 2026 16:44:46 +0530 Subject: [PATCH 43/51] strip crlf from multipart content-type header (#3432) --- .../feign/form/multipart/AbstractWriter.java | 15 ++++++++++- .../form/multipart/SingleParameterWriter.java | 2 +- .../form/multipart/AbstractWriterTest.java | 25 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/form/src/main/java/feign/form/multipart/AbstractWriter.java b/form/src/main/java/feign/form/multipart/AbstractWriter.java index d445974a36..0869430145 100644 --- a/form/src/main/java/feign/form/multipart/AbstractWriter.java +++ b/form/src/main/java/feign/form/multipart/AbstractWriter.java @@ -89,7 +89,7 @@ protected void writeFileMetadata( .append(contentDespositionBuilder.toString()) .append(CRLF) .append("Content-Type: ") - .append(fileContentType) + .append(stripCrlf(fileContentType)) .append(CRLF) .append("Content-Transfer-Encoding: binary") .append(CRLF) @@ -111,4 +111,17 @@ protected void writeFileMetadata( protected static String escapeHeaderParameter(String value) { return value.replace("\r", "%0D").replace("\n", "%0A").replace("\"", "%22"); } + + /** + * Removes carriage return and line feed from a media type so an attacker-supplied content type + * cannot inject extra part headers or boundaries. Unlike a quoted parameter the {@code + * Content-Type} value is not quoted, so it cannot be percent-encoded without corrupting a + * legitimate media type; the control characters are dropped instead. + * + * @param contentType the raw content type value. + * @return the content type with CR and LF removed. + */ + protected static String stripCrlf(String contentType) { + return contentType.replace("\r", "").replace("\n", ""); + } } diff --git a/form/src/main/java/feign/form/multipart/SingleParameterWriter.java b/form/src/main/java/feign/form/multipart/SingleParameterWriter.java index 55eab20204..0866ee2ada 100644 --- a/form/src/main/java/feign/form/multipart/SingleParameterWriter.java +++ b/form/src/main/java/feign/form/multipart/SingleParameterWriter.java @@ -58,7 +58,7 @@ protected void writeWithContentType(Output output, String key, Object value, Str .append('"') .append(CRLF) .append("Content-Type: ") - .append(contentTypeHeader) + .append(stripCrlf(contentTypeHeader)) .append(CRLF) .append(CRLF) .append(value.toString()) diff --git a/form/src/test/java/feign/form/multipart/AbstractWriterTest.java b/form/src/test/java/feign/form/multipart/AbstractWriterTest.java index 05e40094cf..5b00f778f2 100644 --- a/form/src/test/java/feign/form/multipart/AbstractWriterTest.java +++ b/form/src/test/java/feign/form/multipart/AbstractWriterTest.java @@ -46,4 +46,29 @@ void parameterNameWithCrlfIsEscaped() { assertThat(written).contains("name=\"a%22%0D%0AX-Injected: 1\""); assertThat(written).doesNotContain("\r\nX-Injected: 1"); } + + @Test + void fileContentTypeWithCrlfIsStripped() { + Output output = new Output(UTF_8); + FormData formData = + new FormData("text/plain\r\nX-Injected: 1", "file.txt", "body".getBytes(UTF_8)); + + new FormDataWriter().write(output, "boundary", "file", formData); + String written = new String(output.toByteArray(), UTF_8); + + assertThat(written).contains("Content-Type: text/plainX-Injected: 1"); + assertThat(written).doesNotContain("\r\nX-Injected: 1"); + } + + @Test + void parameterContentTypeWithCrlfIsStripped() { + Output output = new Output(UTF_8); + + new SingleParameterWriter() + .writeWithContentType(output, "name", "value", "text/plain\r\nX-Injected: 1"); + String written = new String(output.toByteArray(), UTF_8); + + assertThat(written).contains("Content-Type: text/plainX-Injected: 1"); + assertThat(written).doesNotContain("\r\nX-Injected: 1"); + } } From d05f71a07112c459fe720cd9dbdcdea14f4b5a0f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 05:13:20 +0000 Subject: [PATCH 44/51] build(deps-dev): Bump com.gradle:develocity-maven-extension Bumps com.gradle:develocity-maven-extension from 2.4.1 to 2.4.2. --- updated-dependencies: - dependency-name: com.gradle:develocity-maven-extension dependency-version: 2.4.2 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .mvn/extensions.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index 1c05e35670..bfbb33c261 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -19,7 +19,7 @@ com.gradle develocity-maven-extension - 2.4.1 + 2.4.2 com.gradle From d32f2082b1f1962ae401147252ec6dd776a83d2a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 05:13:27 +0000 Subject: [PATCH 45/51] build(deps): Bump org.openrewrite.recipe:rewrite-testing-frameworks Bumps [org.openrewrite.recipe:rewrite-testing-frameworks](https://github.com/openrewrite/rewrite-testing-frameworks) from 3.38.0 to 3.40.0. - [Release notes](https://github.com/openrewrite/rewrite-testing-frameworks/releases) - [Commits](https://github.com/openrewrite/rewrite-testing-frameworks/compare/v3.38.0...v3.40.0) --- updated-dependencies: - dependency-name: org.openrewrite.recipe:rewrite-testing-frameworks dependency-version: 3.40.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3fd3304d2f..68a5c2be71 100644 --- a/pom.xml +++ b/pom.xml @@ -206,7 +206,7 @@ 1.2.8 4.0.0 6.42.0 - 3.38.0 + 3.40.0 3.37.0 0.26.1 1.0 From 711fa7ae02cb5b0f4fec43ebbdd9b7ee8a03f112 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 05:13:08 +0000 Subject: [PATCH 46/51] build(deps-dev): Bump vertx.version in /vertx/feign-vertx5-test Bumps `vertx.version` from 5.1.2 to 5.1.3. Updates `io.vertx:vertx-junit5` from 5.1.2 to 5.1.3 - [Commits](https://github.com/eclipse-vertx/vertx-junit5/compare/5.1.2...5.1.3) Updates `io.vertx:vertx-web-client` from 5.1.2 to 5.1.3 - [Commits](https://github.com/vert-x3/vertx-web/compare/5.1.2...5.1.3) --- updated-dependencies: - dependency-name: io.vertx:vertx-junit5 dependency-version: 5.1.3 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: io.vertx:vertx-web-client dependency-version: 5.1.3 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- vertx/feign-vertx5-test/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vertx/feign-vertx5-test/pom.xml b/vertx/feign-vertx5-test/pom.xml index 4f9d853346..2798249acf 100644 --- a/vertx/feign-vertx5-test/pom.xml +++ b/vertx/feign-vertx5-test/pom.xml @@ -30,7 +30,7 @@ Tests with Vertx 5.x. - 5.1.2 + 5.1.3 From b46b35126c2d1ba955904eee7e3b1bb9ec101279 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 05:15:09 +0000 Subject: [PATCH 47/51] build(deps): Bump org.openrewrite.recipe:rewrite-migrate-java Bumps [org.openrewrite.recipe:rewrite-migrate-java](https://github.com/openrewrite/rewrite-migrate-java) from 3.37.0 to 3.38.0. - [Release notes](https://github.com/openrewrite/rewrite-migrate-java/releases) - [Commits](https://github.com/openrewrite/rewrite-migrate-java/compare/v3.37.0...v3.38.0) --- updated-dependencies: - dependency-name: org.openrewrite.recipe:rewrite-migrate-java dependency-version: 3.38.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 68a5c2be71..279862715e 100644 --- a/pom.xml +++ b/pom.xml @@ -207,7 +207,7 @@ 4.0.0 6.42.0 3.40.0 - 3.37.0 + 3.38.0 0.26.1 1.0 From b547fb5b7e7b75352171ec5915bc6237d6ddf34c Mon Sep 17 00:00:00 2001 From: seonwoojung Date: Tue, 23 Jun 2026 20:14:03 +0900 Subject: [PATCH 48/51] Return response body in FeignException from errorReading (#2618) (#3415) * Return response body in FeignException from errorReading (#2618) FeignException.errorReading passed request.body() and request.headers() into the constructor's responseBody/responseHeaders parameters, so FeignException.responseBody()/contentUTF8()/responseHeaders() exposed the request data instead of the response when a read failure occurred while decoding a response. Read the response body (mirroring errorStatus, tolerating a streamed or already-consumed body) and pass the response headers instead. Signed-off-by: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> * test: assert response body in errorReading FeignException (#2618) The pre-existing throwsFeignException{Including,Without}Body tests in FeignTest, AsyncFeignTest and FeignUnderAsyncTest asserted the old behavior where FeignException.contentUTF8() returned the *request* body on the errorReading path. #2618 corrects this to return the *response* body, so update the assertions accordingly (and enqueue an empty response for the without-body case). Fixes the failing ci/circleci: pr-build. Signed-off-by: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> * test: assert response body in errorReading for async client modules (#2618) The okhttp/java11/hc5 async client tests still asserted the pre-#2618 request body in throwsFeignExceptionIncludingBody, turning ci/circleci: pr-build red. Align them with core to expect the response body, matching the new errorReading behavior. Signed-off-by: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> --------- Signed-off-by: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> --- core/src/main/java/feign/FeignException.java | 11 +++++++++-- core/src/test/java/feign/AsyncFeignTest.java | 3 ++- core/src/test/java/feign/FeignExceptionTest.java | 6 ++++++ core/src/test/java/feign/FeignTest.java | 5 +++-- core/src/test/java/feign/FeignUnderAsyncTest.java | 5 +++-- .../java/feign/hc5/AsyncApacheHttp5ClientTest.java | 3 ++- .../feign/http2client/test/Http2ClientAsyncTest.java | 3 ++- .../test/java/feign/okhttp/OkHttpClientAsyncTest.java | 3 ++- 8 files changed, 29 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/feign/FeignException.java b/core/src/main/java/feign/FeignException.java index b7ea794cae..0779598c61 100644 --- a/core/src/main/java/feign/FeignException.java +++ b/core/src/main/java/feign/FeignException.java @@ -177,13 +177,20 @@ public String contentUTF8() { } static FeignException errorReading(Request request, Response response, IOException cause) { + byte[] body = {}; + try { + if (response.body() != null) { + body = Util.toByteArray(response.body().asInputStream()); + } + } catch (IOException ignored) { // NOPMD + } return new FeignException( response.status(), format("%s reading %s %s", cause.getMessage(), request.httpMethod(), request.url()), request, cause, - request.body(), - request.headers()); + body, + response.headers()); } public static FeignException errorStatus(String methodKey, Response response) { diff --git a/core/src/test/java/feign/AsyncFeignTest.java b/core/src/test/java/feign/AsyncFeignTest.java index 300385be64..a67242b139 100644 --- a/core/src/test/java/feign/AsyncFeignTest.java +++ b/core/src/test/java/feign/AsyncFeignTest.java @@ -621,7 +621,8 @@ void throwsFeignExceptionIncludingBody() throws Throwable { } catch (FeignException e) { assertThat(e.getMessage()) .contains("timeout reading POST http://localhost:" + server.getPort() + "/"); - assertThat(e.contentUTF8()).isEqualTo("Request body"); + // After #2618 the FeignException carries the response body, not the request body. + assertThat(e.contentUTF8()).isEqualTo("success!"); return; } fail(""); diff --git a/core/src/test/java/feign/FeignExceptionTest.java b/core/src/test/java/feign/FeignExceptionTest.java index f86d35fe19..e585d35e7c 100644 --- a/core/src/test/java/feign/FeignExceptionTest.java +++ b/core/src/test/java/feign/FeignExceptionTest.java @@ -43,16 +43,22 @@ void canCreateWithRequestAndResponse() { StandardCharsets.UTF_8, null); + Map> responseHeaders = + Collections.singletonMap("content-type", Collections.singletonList("application/json")); Response response = Response.builder() .status(400) .body("response".getBytes(StandardCharsets.UTF_8)) + .headers(responseHeaders) .request(request) .build(); FeignException exception = FeignException.errorReading(request, response, new IOException("socket closed")); assertThat(exception.responseBody()).isNotEmpty(); + // the exception must carry the response body, not the request body (gh-2618) + assertThat(exception.contentUTF8()).isEqualTo("response"); + assertThat(exception.responseHeaders()).containsKeys("content-type"); assertThat(exception.hasRequest()).isTrue(); assertThat(exception.request()).isNotNull(); } diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 47a348bfaf..03acf67a51 100755 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -607,13 +607,14 @@ void throwsFeignExceptionIncludingBody() { } catch (FeignException e) { assertThat(e.getMessage()) .isEqualTo("timeout reading POST http://localhost:" + server.getPort() + "/"); - assertThat(e.contentUTF8()).isEqualTo("Request body"); + // After #2618 the FeignException carries the response body, not the request body. + assertThat(e.contentUTF8()).isEqualTo("success!"); } } @Test void throwsFeignExceptionWithoutBody() { - server.enqueue(new MockResponse().setBody("success!")); + server.enqueue(new MockResponse()); TestInterface api = Feign.builder() diff --git a/core/src/test/java/feign/FeignUnderAsyncTest.java b/core/src/test/java/feign/FeignUnderAsyncTest.java index f18fd5d747..ac150ae370 100644 --- a/core/src/test/java/feign/FeignUnderAsyncTest.java +++ b/core/src/test/java/feign/FeignUnderAsyncTest.java @@ -493,13 +493,14 @@ void throwsFeignExceptionIncludingBody() { } catch (FeignException e) { assertThat(e.getMessage()) .isEqualTo("timeout reading POST http://localhost:" + server.getPort() + "/"); - assertThat(e.contentUTF8()).isEqualTo("Request body"); + // After #2618 the FeignException carries the response body, not the request body. + assertThat(e.contentUTF8()).isEqualTo("success!"); } } @Test void throwsFeignExceptionWithoutBody() { - server.enqueue(new MockResponse().setBody("success!")); + server.enqueue(new MockResponse()); TestInterface api = AsyncFeign.builder() diff --git a/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java b/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java index 6d5902d9c0..3b3a4f33cf 100644 --- a/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java +++ b/hc5/src/test/java/feign/hc5/AsyncApacheHttp5ClientTest.java @@ -535,7 +535,8 @@ 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"); + // After #2618 the FeignException carries the response body, not the request body. + assertThat(e.contentUTF8()).isEqualTo("success!"); return; } fail(""); diff --git a/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java b/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java index 0d3cd6570b..c5a29c43e9 100644 --- a/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java +++ b/java11/src/test/java/feign/http2client/test/Http2ClientAsyncTest.java @@ -529,7 +529,8 @@ 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"); + // After #2618 the FeignException carries the response body, not the request body. + assertThat(e.contentUTF8()).isEqualTo("success!"); return; } fail(""); diff --git a/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java b/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java index 79150b86b9..5b2fbe3542 100644 --- a/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java +++ b/okhttp/src/test/java/feign/okhttp/OkHttpClientAsyncTest.java @@ -528,7 +528,8 @@ 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"); + // After #2618 the FeignException carries the response body, not the request body. + assertThat(e.contentUTF8()).isEqualTo("success!"); return; } fail(""); From 5cd6f57e0301b55dc4e953f092e141b59fd8554b Mon Sep 17 00:00:00 2001 From: alhuda <50839256+alhudz@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:46:36 +0530 Subject: [PATCH 49/51] treat invalid expression value modifier as a literal (#3437) --- .../main/java/feign/template/Expressions.java | 28 +++++++++++++------ .../test/java/feign/RequestTemplateTest.java | 11 ++++++++ .../java/feign/template/ExpressionsTest.java | 12 ++++++++ 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/feign/template/Expressions.java b/core/src/main/java/feign/template/Expressions.java index 1368364d59..27b5f5e545 100644 --- a/core/src/main/java/feign/template/Expressions.java +++ b/core/src/main/java/feign/template/Expressions.java @@ -22,6 +22,7 @@ import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; public final class Expressions { @@ -117,15 +118,26 @@ public static Expression create(final String value) { } } - /* check for an operator */ - if (PATH_STYLE_OPERATOR.equalsIgnoreCase(operator)) { - return new PathStyleExpression(variableName, variablePattern); - } + /* + * The value modifier after the ':' is compiled as a regular expression. When the chunk is a + * dynamic value (for example a header-map value that happens to contain '{' and ':') the + * modifier is not a valid pattern, so treat the chunk as a literal instead of letting the + * PatternSyntaxException escape. + */ + try { + /* check for an operator */ + if (PATH_STYLE_OPERATOR.equalsIgnoreCase(operator)) { + return new PathStyleExpression(variableName, variablePattern); + } - /* default to simple */ - return SimpleExpression.isSimpleExpression(value) - ? new SimpleExpression(variableName, variablePattern) - : null; // Return null if it can't be validated as a Simple Expression -- Probably a Literal + /* default to simple */ + // Return null if it can't be validated as a Simple Expression -- Probably a Literal + return SimpleExpression.isSimpleExpression(value) + ? new SimpleExpression(variableName, variablePattern) + : null; + } catch (PatternSyntaxException e) { + return null; + } } private static String stripBraces(String expression) { diff --git a/core/src/test/java/feign/RequestTemplateTest.java b/core/src/test/java/feign/RequestTemplateTest.java index d66b8db289..622e9cf942 100644 --- a/core/src/test/java/feign/RequestTemplateTest.java +++ b/core/src/test/java/feign/RequestTemplateTest.java @@ -218,6 +218,17 @@ void resolveTemplateWithHeaderContainingJsonLiteral() { assertThat(template).hasHeaders(entry("A-Header", Collections.singletonList(json))); } + /** A header-map value containing brackets and a colon must not be parsed as a regex. */ + @Test + void resolveTemplateWithHeaderMapValueContainingPatternModifier() { + String value = "{range:[1:10}"; + RequestTemplate template = + new RequestTemplate().method(HttpMethod.GET).header("A-Header", value); + + template.resolve(new LinkedHashMap<>()); + assertThat(template).hasHeaders(entry("A-Header", Collections.singletonList(value))); + } + @Test void resolveTemplateWithHeaderWithJson() { String json = "{ \"string\": \"val\", \"string2\": \"this should not be truncated\"}"; diff --git a/core/src/test/java/feign/template/ExpressionsTest.java b/core/src/test/java/feign/template/ExpressionsTest.java index 77241770f5..451a3c18a9 100644 --- a/core/src/test/java/feign/template/ExpressionsTest.java +++ b/core/src/test/java/feign/template/ExpressionsTest.java @@ -84,6 +84,18 @@ void malformedExpression() { } } + @Test + void invalidValueModifierIsTreatedAsLiteral() { + // The text after ':' is compiled as a regex; an invalid one must not escape as a + // PatternSyntaxException, the chunk is a literal instead (Expressions.create returns null). + assertThatNoException().isThrownBy(() -> Expressions.create("{range:[1:10}")); + assertThat(Expressions.create("{a:[}")).isNull(); + assertThat(Expressions.create("{a:(}")).isNull(); + + // a valid value modifier still produces an expression + assertThat(Expressions.create("{id:[0-9]+}")).isNotNull(); + } + @Test void malformedBodyTemplate() { String bodyTemplate = "{" + "a".repeat(65536) + "}"; From 3062e80f3d4c951ac7b975b4d73ae215d444f9b4 Mon Sep 17 00:00:00 2001 From: alhuda <50839256+alhudz@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:46:41 +0530 Subject: [PATCH 50/51] encode collection values in urlencoded form bodies (#3436) --- .../feign/form/UrlencodedFormContentProcessor.java | 5 ++++- .../form/UrlencodedFormContentProcessorTest.java | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/form/src/main/java/feign/form/UrlencodedFormContentProcessor.java b/form/src/main/java/feign/form/UrlencodedFormContentProcessor.java index 14fb8166ec..6c82a5c9d2 100644 --- a/form/src/main/java/feign/form/UrlencodedFormContentProcessor.java +++ b/form/src/main/java/feign/form/UrlencodedFormContentProcessor.java @@ -106,7 +106,10 @@ private CharSequence createKeyValuePair( private CharSequence createKeyValuePair( CollectionFormat collectionFormat, String key, Stream values, Charset charset) { val stringValues = - values.filter(Objects::nonNull).map(Object::toString).collect(Collectors.toList()); + values + .filter(Objects::nonNull) + .map(value -> encode(value, charset)) + .collect(Collectors.toList()); return collectionFormat.join(key, stringValues, charset); } } diff --git a/form/src/test/java/feign/form/UrlencodedFormContentProcessorTest.java b/form/src/test/java/feign/form/UrlencodedFormContentProcessorTest.java index 17cf99ea68..be915fab92 100644 --- a/form/src/test/java/feign/form/UrlencodedFormContentProcessorTest.java +++ b/form/src/test/java/feign/form/UrlencodedFormContentProcessorTest.java @@ -46,6 +46,20 @@ void collectionValueUsesDefaultExplodedCollectionFormat() { Arrays.asList("one", "two"), Client::map); } + @Test + void arrayValueEncodesReservedCharacters() { + assertEncodedBody( + "from=%2B987654321&to=%2B123456789&tags=a%26b%3Dc&tags=d", + new String[] {"a&b=c", "d"}, Client::map); + } + + @Test + void collectionValueEncodesReservedCharacters() { + assertEncodedBody( + "from=%2B987654321&to=%2B123456789&tags=a%26b%3Dc&tags=d", + Arrays.asList("a&b=c", "d"), Client::map); + } + @Test void arrayValueUsesCsvCollectionFormat() { assertEncodedBody( From baeba5a7fa2245fb1987dbce8fbcf1468768445c Mon Sep 17 00:00:00 2001 From: alhuda <50839256+alhudz@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:46:47 +0530 Subject: [PATCH 51/51] encode soap request body with the configured charset (#3433) --- .../src/main/java/feign/soap/SOAPEncoder.java | 2 +- .../test/java/feign/soap/SOAPCodecTest.java | 34 +++++++++++++++++++ .../src/main/java/feign/soap/SOAPEncoder.java | 2 +- .../test/java/feign/soap/SOAPCodecTest.java | 34 +++++++++++++++++++ 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java b/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java index 709b7f2a13..2b4a59cabb 100644 --- a/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java +++ b/soap-jakarta/src/main/java/feign/soap/SOAPEncoder.java @@ -131,7 +131,7 @@ public void encode(Object object, Type bodyType, RequestTemplate template) { } else { soapMessage.writeTo(bos); } - template.body(bos.toString()); + template.body(bos.toByteArray(), charsetEncoding); } 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..d0be7ef62f 100644 --- a/soap-jakarta/src/test/java/feign/soap/SOAPCodecTest.java +++ b/soap-jakarta/src/test/java/feign/soap/SOAPCodecTest.java @@ -131,6 +131,40 @@ void encodesSoapWithCustomJAXBMarshallerEncoding() { assertThat(template).hasBody(utf16Bytes); } + @Test + void encodesSoapWithNonAsciiContentInConfiguredCharset() { + JAXBContextFactory jaxbContextFactory = + new JAXBContextFactory.Builder().withMarshallerJAXBEncoding("UTF-16").build(); + + Encoder encoder = + new SOAPEncoder.Builder() + .withJAXBContextFactory(jaxbContextFactory) + .withCharsetEncoding(StandardCharsets.UTF_16) + .build(); + + GetPrice mock = new GetPrice(); + mock.item = new Item(); + mock.item.value = "Café"; + + RequestTemplate template = new RequestTemplate(); + encoder.encode(mock, GetPrice.class, template); + + String soapEnvelop = + """ + \ + \ + \ + \ + \ + Café\ + \ + \ + \ + """; + byte[] utf16Bytes = soapEnvelop.getBytes(StandardCharsets.UTF_16LE); + assertThat(template).hasBody(utf16Bytes); + } + @Test void encodesSoapWithCustomJAXBSchemaLocation() { JAXBContextFactory jaxbContextFactory = diff --git a/soap/src/main/java/feign/soap/SOAPEncoder.java b/soap/src/main/java/feign/soap/SOAPEncoder.java index 24f10da917..d22d97fefa 100644 --- a/soap/src/main/java/feign/soap/SOAPEncoder.java +++ b/soap/src/main/java/feign/soap/SOAPEncoder.java @@ -135,7 +135,7 @@ public void encode(Object object, Type bodyType, RequestTemplate template) { } else { soapMessage.writeTo(bos); } - template.body(new String(bos.toByteArray())); + template.body(bos.toByteArray(), charsetEncoding); } 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..bddc2cdc27 100644 --- a/soap/src/test/java/feign/soap/SOAPCodecTest.java +++ b/soap/src/test/java/feign/soap/SOAPCodecTest.java @@ -131,6 +131,40 @@ void encodesSoapWithCustomJAXBMarshallerEncoding() { assertThat(template).hasBody(utf16Bytes); } + @Test + void encodesSoapWithNonAsciiContentInConfiguredCharset() { + JAXBContextFactory jaxbContextFactory = + new JAXBContextFactory.Builder().withMarshallerJAXBEncoding("UTF-16").build(); + + Encoder encoder = + new SOAPEncoder.Builder() + .withJAXBContextFactory(jaxbContextFactory) + .withCharsetEncoding(StandardCharsets.UTF_16) + .build(); + + GetPrice mock = new GetPrice(); + mock.item = new Item(); + mock.item.value = "Café"; + + RequestTemplate template = new RequestTemplate(); + encoder.encode(mock, GetPrice.class, template); + + String soapEnvelop = + """ + \ + \ + \ + \ + \ + Café\ + \ + \ + \ + """; + byte[] utf16Bytes = soapEnvelop.getBytes(StandardCharsets.UTF_16LE); + assertThat(template).hasBody(utf16Bytes); + } + @Test void encodesSoapWithCustomJAXBSchemaLocation() { JAXBContextFactory jaxbContextFactory =