diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index 8d9d569e..2ea8699f 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -1,26 +1,28 @@ |=== |Name | Default | Description -|feign.circuitbreaker.enabled | false | If true, an OpenFeign client will be wrapped with a Spring Cloud CircuitBreaker circuit breaker. +|feign.autoconfiguration.jackson.enabled | `false` | If true, PageJacksonModule and SortJacksonModule bean will be provided for Jackson page decoding. +|feign.circuitbreaker.enabled | `false` | If true, an OpenFeign client will be wrapped with a Spring Cloud CircuitBreaker circuit breaker. |feign.client.config | | -|feign.client.decode-slash | true | Feign clients do not encode slash `/` characters by default. To change this behavior, set the `decodeSlash` to `false`. -|feign.client.default-config | default | -|feign.client.default-to-properties | true | -|feign.compression.request.enabled | false | Enables the request sent by Feign to be compressed. -|feign.compression.request.mime-types | [text/xml, application/xml, application/json] | The list of supported mime types. -|feign.compression.request.min-request-size | 2048 | The minimum threshold content size. -|feign.compression.response.enabled | false | Enables the response from Feign to be compressed. -|feign.compression.response.useGzipDecoder | false | Enables the default gzip decoder to be used. -|feign.httpclient.connection-timeout | 2000 | -|feign.httpclient.connection-timer-repeat | 3000 | -|feign.httpclient.disable-ssl-validation | false | -|feign.httpclient.enabled | true | Enables the use of the Apache HTTP Client by Feign. -|feign.httpclient.follow-redirects | true | -|feign.httpclient.max-connections | 200 | -|feign.httpclient.max-connections-per-route | 50 | -|feign.httpclient.time-to-live | 900 | +|feign.client.decode-slash | `true` | Feign clients do not encode slash `/` characters by default. To change this behavior, set the `decodeSlash` to `false`. +|feign.client.default-config | `default` | +|feign.client.default-to-properties | `true` | +|feign.compression.request.enabled | `false` | Enables the request sent by Feign to be compressed. +|feign.compression.request.mime-types | `[text/xml, application/xml, application/json]` | The list of supported mime types. +|feign.compression.request.min-request-size | `2048` | The minimum threshold content size. +|feign.compression.response.enabled | `false` | Enables the response from Feign to be compressed. +|feign.compression.response.useGzipDecoder | `false` | Enables the default gzip decoder to be used. +|feign.encoder.charset-from-content-type | `false` | +|feign.httpclient.connection-timeout | `2000` | +|feign.httpclient.connection-timer-repeat | `3000` | +|feign.httpclient.disable-ssl-validation | `false` | +|feign.httpclient.enabled | `true` | Enables the use of the Apache HTTP Client by Feign. +|feign.httpclient.follow-redirects | `true` | +|feign.httpclient.max-connections | `200` | +|feign.httpclient.max-connections-per-route | `50` | +|feign.httpclient.time-to-live | `900` | |feign.httpclient.time-to-live-unit | | -|feign.hystrix.enabled | false | If true, an OpenFeign client will be wrapped with a Hystrix circuit breaker. -|feign.okhttp.enabled | false | Enables the use of the OK HTTP Client by Feign. +|feign.hystrix.enabled | `false` | If true, an OpenFeign client will be wrapped with a Hystrix circuit breaker. +|feign.okhttp.enabled | `false` | Enables the use of the OK HTTP Client by Feign. |=== \ No newline at end of file diff --git a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc index cac6f58f..9317a98e 100644 --- a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc +++ b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc @@ -277,6 +277,12 @@ public FeignClientConfigurer feignClientConfigurer() { TIP: By default, Feign clients do not encode slash `/` characters. You can change this behaviour, by setting the value of `feign.client.decodeSlash` to `false`. +==== `SpringEncoder` configuration + +In the `SpringEncoder` that we provide, we set `null` charset for binary content types and `UTF-8` for all the other ones. + +You can modify this behaviour to derive the charset from the `Content-Type` header charset instead by setting the value of `feign.encoder.charset-from-content-type` to `true`. + [[timeout-handling]] === Timeout Handling diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java index 2250ee29..9c563c28 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2020 the original author or authors. + * Copyright 2013-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,6 +55,7 @@ import org.springframework.cloud.commons.httpclient.ApacheHttpClientFactory; import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory; import org.springframework.cloud.commons.httpclient.OkHttpClientFactory; import org.springframework.cloud.openfeign.support.DefaultGzipDecoderConfiguration; +import org.springframework.cloud.openfeign.support.FeignEncoderProperties; import org.springframework.cloud.openfeign.support.FeignHttpClientProperties; import org.springframework.cloud.openfeign.support.PageJacksonModule; import org.springframework.cloud.openfeign.support.SortJacksonModule; @@ -71,11 +72,12 @@ import org.springframework.data.domain.Sort; * @author Grzegorz Poznachowski * @author Nikita Konev * @author Tim Peeters + * @author Olga Maciaszek-Sharma */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(Feign.class) @EnableConfigurationProperties({ FeignClientProperties.class, - FeignHttpClientProperties.class }) + FeignHttpClientProperties.class, FeignEncoderProperties.class }) @Import(DefaultGzipDecoderConfiguration.class) public class FeignAutoConfiguration { diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java index 4d6822f6..b6fa1ac3 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2020 the original author or authors. + * Copyright 2013-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,6 +45,7 @@ import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; import org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer; import org.springframework.cloud.openfeign.support.AbstractFormWriter; +import org.springframework.cloud.openfeign.support.FeignEncoderProperties; import org.springframework.cloud.openfeign.support.PageableSpringEncoder; import org.springframework.cloud.openfeign.support.ResponseEntityDecoder; import org.springframework.cloud.openfeign.support.SpringDecoder; @@ -63,6 +64,7 @@ import static feign.form.ContentType.MULTIPART; * @author Dave Syer * @author Venil Noronha * @author Darren Foong + * @author Olga Maciaszek-Sharma */ @Configuration(proxyBeanMethods = false) public class FeignClientsConfiguration { @@ -85,6 +87,9 @@ public class FeignClientsConfiguration { @Autowired(required = false) private FeignClientProperties feignClientProperties; + @Autowired(required = false) + private FeignEncoderProperties encoderProperties; + @Bean @ConditionalOnMissingBean public Decoder feignDecoder() { @@ -96,7 +101,7 @@ public class FeignClientsConfiguration { @ConditionalOnMissingBean @ConditionalOnMissingClass("org.springframework.data.domain.Pageable") public Encoder feignEncoder(ObjectProvider formWriterProvider) { - return springEncoder(formWriterProvider); + return springEncoder(formWriterProvider, encoderProperties); } @Bean @@ -105,7 +110,7 @@ public class FeignClientsConfiguration { public Encoder feignEncoderPageable( ObjectProvider formWriterProvider) { PageableSpringEncoder encoder = new PageableSpringEncoder( - springEncoder(formWriterProvider)); + springEncoder(formWriterProvider, encoderProperties)); if (springDataWebProperties != null) { encoder.setPageParameter( @@ -162,15 +167,17 @@ public class FeignClientsConfiguration { }; } - private Encoder springEncoder(ObjectProvider formWriterProvider) { + private Encoder springEncoder(ObjectProvider formWriterProvider, + FeignEncoderProperties encoderProperties) { AbstractFormWriter formWriter = formWriterProvider.getIfAvailable(); if (formWriter != null) { return new SpringEncoder(new SpringPojoFormEncoder(formWriter), - this.messageConverters); + this.messageConverters, encoderProperties); } else { - return new SpringEncoder(new SpringFormEncoder(), this.messageConverters); + return new SpringEncoder(new SpringFormEncoder(), this.messageConverters, + encoderProperties); } } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/FeignEncoderProperties.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/FeignEncoderProperties.java new file mode 100644 index 00000000..19de42b1 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/FeignEncoderProperties.java @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.openfeign.support; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Properties for {@link SpringEncoder}. + * + * @author Olga Maciaszek-Sharma + * + * @since 2.2.8 + */ +@ConfigurationProperties("feign.encoder") +public class FeignEncoderProperties { + + /** + * Indicates whether the charset should be derived from the {@code Content-Type} header. + */ + private boolean charsetFromContentType = false; + + public boolean isCharsetFromContentType() { + return charsetFromContentType; + } + + public void setCharsetFromContentType(boolean charsetFromContentType) { + this.charsetFromContentType = charsetFromContentType; + } + +} diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringEncoder.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringEncoder.java index ed99a2b9..257ca87c 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringEncoder.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2020 the original author or authors. + * Copyright 2013-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,10 +22,10 @@ import java.io.OutputStream; import java.lang.reflect.Type; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Collection; -import java.util.Objects; +import java.util.stream.Stream; -import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; @@ -55,7 +55,9 @@ import static org.springframework.cloud.openfeign.support.FeignUtils.getHttpHead * @author Ahmad Mozafarnia * @author Aaron Whiteside * @author Darren Foong + * @author Olga Maciaszek-Sharma */ +@SuppressWarnings("rawtypes") public class SpringEncoder implements Encoder { private static final Log log = LogFactory.getLog(SpringEncoder.class); @@ -64,15 +66,23 @@ public class SpringEncoder implements Encoder { private final ObjectFactory messageConverters; + private final FeignEncoderProperties encoderProperties; + public SpringEncoder(ObjectFactory messageConverters) { - this.springFormEncoder = new SpringFormEncoder(); - this.messageConverters = messageConverters; + this(new SpringFormEncoder(), messageConverters); } public SpringEncoder(SpringFormEncoder springFormEncoder, ObjectFactory messageConverters) { + this(springFormEncoder, messageConverters, new FeignEncoderProperties()); + } + + public SpringEncoder(SpringFormEncoder springFormEncoder, + ObjectFactory messageConverters, + FeignEncoderProperties encoderProperties) { this.springFormEncoder = springFormEncoder; this.messageConverters = messageConverters; + this.encoderProperties = encoderProperties; } @Override @@ -89,7 +99,7 @@ public class SpringEncoder implements Encoder { requestContentType = MediaType.valueOf(type); } - if (Objects.equals(requestContentType, MediaType.MULTIPART_FORM_DATA)) { + if (isMultipartType(requestContentType)) { this.springFormEncoder.encode(requestBody, bodyType, request); return; } @@ -100,58 +110,76 @@ public class SpringEncoder implements Encoder { + "should be specified as MediaType.MULTIPART_FORM_DATA_VALUE"); } } - - for (HttpMessageConverter messageConverter : this.messageConverters - .getObject().getConverters()) { - FeignOutputMessage outputMessage; - try { - if (messageConverter instanceof GenericHttpMessageConverter) { - outputMessage = checkAndWrite(requestBody, bodyType, - requestContentType, - (GenericHttpMessageConverter) messageConverter, request); - } - else { - outputMessage = checkAndWrite(requestBody, requestContentType, - messageConverter, request); - } - } - catch (IOException | HttpMessageConversionException ex) { - throw new EncodeException("Error converting request body", ex); - } - if (outputMessage != null) { - // clear headers - request.headers(null); - // converters can modify headers, so update the request - // with the modified headers - request.headers(getHeaders(outputMessage.getHeaders())); - - // do not use charset for binary data and protobuf - Charset charset; - if (messageConverter instanceof ByteArrayHttpMessageConverter) { - charset = null; - } - else if (messageConverter instanceof ProtobufHttpMessageConverter - && ProtobufHttpMessageConverter.PROTOBUF.isCompatibleWith( - outputMessage.getHeaders().getContentType())) { - charset = null; - } - else { - charset = StandardCharsets.UTF_8; - } - request.body(Request.Body.encoded( - outputMessage.getOutputStream().toByteArray(), charset)); - return; - } - } - String message = "Could not write request: no suitable HttpMessageConverter " - + "found for request type [" + requestBody.getClass().getName() + "]"; - if (requestContentType != null) { - message += " and content type [" + requestContentType + "]"; - } - throw new EncodeException(message); + encodeWithMessageConverter(requestBody, bodyType, request, + requestContentType); } } + private void encodeWithMessageConverter(Object requestBody, Type bodyType, + RequestTemplate request, MediaType requestContentType) { + for (HttpMessageConverter messageConverter : this.messageConverters.getObject() + .getConverters()) { + FeignOutputMessage outputMessage; + try { + if (messageConverter instanceof GenericHttpMessageConverter) { + outputMessage = checkAndWrite(requestBody, bodyType, + requestContentType, + (GenericHttpMessageConverter) messageConverter, request); + } + else { + outputMessage = checkAndWrite(requestBody, requestContentType, + messageConverter, request); + } + } + catch (IOException | HttpMessageConversionException ex) { + throw new EncodeException("Error converting request body", ex); + } + if (outputMessage != null) { + // clear headers + request.headers(null); + // converters can modify headers, so update the request + // with the modified headers + request.headers(getHeaders(outputMessage.getHeaders())); + + // do not use charset for binary data and protobuf + Charset charset; + + MediaType contentType = outputMessage.getHeaders().getContentType(); + Charset charsetFromContentType = contentType != null + ? contentType.getCharset() : null; + + if (encoderProperties != null + && encoderProperties.isCharsetFromContentType() + && charsetFromContentType != null) { + charset = charsetFromContentType; + } + else if (shouldHaveNullCharset(messageConverter, outputMessage)) { + charset = null; + } + else { + charset = StandardCharsets.UTF_8; + } + request.body(outputMessage.getOutputStream().toByteArray(), charset); + return; + } + } + String message = "Could not write request: no suitable HttpMessageConverter " + + "found for request type [" + requestBody.getClass().getName() + "]"; + if (requestContentType != null) { + message += " and content type [" + requestContentType + "]"; + } + throw new EncodeException(message); + } + + private boolean shouldHaveNullCharset(HttpMessageConverter messageConverter, + FeignOutputMessage outputMessage) { + return binaryContentType(outputMessage) + || messageConverter instanceof ByteArrayHttpMessageConverter + || messageConverter instanceof ProtobufHttpMessageConverter + && ProtobufHttpMessageConverter.PROTOBUF.isCompatibleWith( + outputMessage.getHeaders().getContentType()); + } + @SuppressWarnings("unchecked") private FeignOutputMessage checkAndWrite(Object body, MediaType contentType, HttpMessageConverter converter, RequestTemplate request) throws IOException { @@ -195,6 +223,20 @@ public class SpringEncoder implements Encoder { } } + private boolean isMultipartType(MediaType requestContentType) { + return Arrays.asList(MediaType.MULTIPART_FORM_DATA, MediaType.MULTIPART_MIXED, + MediaType.MULTIPART_RELATED).contains(requestContentType); + } + + private boolean binaryContentType(FeignOutputMessage outputMessage) { + MediaType contentType = outputMessage.getHeaders().getContentType(); + return contentType == null || Stream + .of(MediaType.APPLICATION_CBOR, MediaType.APPLICATION_OCTET_STREAM, + MediaType.APPLICATION_PDF, MediaType.IMAGE_GIF, + MediaType.IMAGE_JPEG, MediaType.IMAGE_PNG) + .anyMatch(mediaType -> mediaType.includes(contentType)); + } + private final class FeignOutputMessage implements HttpOutputMessage { private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/circuitbreaker/CircuitBreakerTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/circuitbreaker/CircuitBreakerTests.java index 84bb2531..4b283b42 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/circuitbreaker/CircuitBreakerTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/circuitbreaker/CircuitBreakerTests.java @@ -30,7 +30,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.web.server.LocalServerPort; import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; import org.springframework.cloud.client.circuitbreaker.ConfigBuilder; @@ -72,9 +71,6 @@ public class CircuitBreakerTests { @Autowired TestClientWithFactory testClientWithFactory; - @LocalServerPort - private int port = 0; - @BeforeAll public static void beforeClass() { System.setProperty("server.port", diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/proto/ProtobufNotInClasspathTest.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/proto/ProtobufNotInClasspathTest.java index fc605c8f..b4cb0ea1 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/proto/ProtobufNotInClasspathTest.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/proto/ProtobufNotInClasspathTest.java @@ -20,7 +20,6 @@ import feign.RequestTemplate; import org.junit.Test; import org.junit.runner.RunWith; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectFactory; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.cloud.openfeign.support.SpringEncoder; @@ -41,12 +40,8 @@ public class ProtobufNotInClasspathTest { @Test public void testEncodeWhenProtobufNotInClasspath() { - ObjectFactory converters = new ObjectFactory() { - @Override - public HttpMessageConverters getObject() throws BeansException { - return new HttpMessageConverters(new StringHttpMessageConverter()); - } - }; + ObjectFactory converters = () -> new HttpMessageConverters( + new StringHttpMessageConverter()); RequestTemplate requestTemplate = new RequestTemplate(); requestTemplate.method(POST); new SpringEncoder(converters).encode("a=b", String.class, requestTemplate); diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/proto/ProtobufSpringEncoderTest.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/proto/ProtobufSpringEncoderTest.java index f35b64ea..5611c874 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/proto/ProtobufSpringEncoderTest.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/proto/ProtobufSpringEncoderTest.java @@ -40,11 +40,9 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentMatchers; import org.mockito.BDDMockito; import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.MockitoJUnitRunner; import org.mockito.stubbing.Answer; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectFactory; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.cloud.openfeign.support.SpringEncoder; @@ -73,7 +71,7 @@ public class ProtobufSpringEncoderTest { .build(); @Test - public void testProtobuf() throws IOException, URISyntaxException { + public void testProtobuf() throws IOException { // protobuf convert to request by feign and ProtobufHttpMessageConverter RequestTemplate requestTemplate = newRequestTemplate(); newEncoder().encode(this.request, Request.class, requestTemplate); @@ -110,12 +108,8 @@ public class ProtobufSpringEncoderTest { } private SpringEncoder newEncoder() { - ObjectFactory converters = new ObjectFactory() { - @Override - public HttpMessageConverters getObject() throws BeansException { - return new HttpMessageConverters(new ProtobufHttpMessageConverter()); - } - }; + ObjectFactory converters = () -> new HttpMessageConverters( + new ProtobufHttpMessageConverter()); return new SpringEncoder(converters); } @@ -126,17 +120,13 @@ public class ProtobufSpringEncoderTest { } private HttpEntity toApacheHttpEntity(RequestTemplate requestTemplate) - throws IOException, URISyntaxException { + throws IOException { final List request = new ArrayList<>(1); - BDDMockito.given(this.httpClient.execute(ArgumentMatchers.any())) - .will(new Answer() { - @Override - public HttpResponse answer(InvocationOnMock invocationOnMock) - throws Throwable { - request.add((HttpUriRequest) invocationOnMock.getArguments()[0]); - return new BasicHttpResponse(new BasicStatusLine( - new ProtocolVersion("http", 1, 1), 200, null)); - } + BDDMockito.given(this.httpClient.execute(ArgumentMatchers.any())) + .will((Answer) invocationOnMock -> { + request.add((HttpUriRequest) invocationOnMock.getArguments()[0]); + return new BasicHttpResponse(new BasicStatusLine( + new ProtocolVersion("http", 1, 1), 200, null)); }); new ApacheHttpClient(this.httpClient).execute( requestTemplate.resolve(new HashMap<>()).request(), diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringEncoderTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringEncoderTests.java index 91c595a8..58068799 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringEncoderTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringEncoderTests.java @@ -36,9 +36,11 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.cloud.openfeign.FeignContext; import org.springframework.cloud.openfeign.encoding.HttpEncoding; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.io.Resource; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; @@ -47,6 +49,7 @@ import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.http.converter.ResourceHttpMessageConverter; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -59,6 +62,7 @@ import static org.springframework.http.HttpHeaders.CONTENT_LENGTH; import static org.springframework.http.HttpHeaders.CONTENT_TYPE; import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE; import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; +import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE; /** * @author Spencer Gibb @@ -79,6 +83,9 @@ public class SpringEncoderTests { @Qualifier("myHttpMessageConverter") private HttpMessageConverter myConverter; + @Autowired + private ApplicationContext applicationContext; + @Autowired @Qualifier("myGenericHttpMessageConverter") private GenericHttpMessageConverter myGenericConverter; @@ -185,6 +192,32 @@ public class SpringEncoderTests { .as("Body content cannot be decoded").contains("hi"); } + @Test + public void testNoCharsetForBinaryFiles() { + Encoder encoder = context.getInstance("test", Encoder.class); + assertThat(encoder).isNotNull(); + RequestTemplate request = new RequestTemplate(); + request.header(CONTENT_TYPE, APPLICATION_OCTET_STREAM_VALUE); + Resource resource = applicationContext.getResource("classpath:dummy.pdf"); + + encoder.encode(resource, Resource.class, request); + + assertThat(request.requestBody().getEncoding()).isEmpty(); + } + + @Test + public void testUTF8CharsetForTextFiles() { + Encoder encoder = context.getInstance("test", Encoder.class); + assertThat(encoder).isNotNull(); + RequestTemplate request = new RequestTemplate(); + request.header(CONTENT_TYPE, TEXT_PLAIN_VALUE); + String test = "test"; + + encoder.encode(test, String.class, request); + + assertThat(request.requestBody().getEncoding().get().name()).isEqualTo("UTF-8"); + } + protected interface TestClient { } @@ -213,6 +246,11 @@ public class SpringEncoderTests { return new MyHttpMessageConverter(); } + @Bean + ResourceHttpMessageConverter resourceHttpMessageConverter() { + return new ResourceHttpMessageConverter(); + } + @Bean GenericHttpMessageConverter myGenericHttpMessageConverter() { return new MyGenericHttpMessageConverter(); diff --git a/spring-cloud-openfeign-core/src/test/resources/dummy.pdf b/spring-cloud-openfeign-core/src/test/resources/dummy.pdf new file mode 100644 index 00000000..774c2ea7 Binary files /dev/null and b/spring-cloud-openfeign-core/src/test/resources/dummy.pdf differ