Pass headers back to feign request.

Spring HttpMessageConverters can modify headers. This change passes the modified headersr back to the feign request, so the proper headers are sent on the request.

 fixes gh-977
This commit is contained in:
Spencer Gibb
2016-04-20 06:46:23 -06:00
parent 33efc60de7
commit e0c67b30f8
4 changed files with 180 additions and 8 deletions

View File

@@ -18,6 +18,8 @@ package org.springframework.cloud.netflix.feign.support;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.http.HttpHeaders;
@@ -35,4 +37,14 @@ public class FeignUtils {
return httpHeaders;
}
static Map<String, Collection<String>> getHeaders(HttpHeaders httpHeaders) {
LinkedHashMap<String, Collection<String>> headers = new LinkedHashMap<>();
for (Map.Entry<String, List<String>> entry : httpHeaders.entrySet()) {
headers.put(entry.getKey(), entry.getValue());
}
return headers;
}
}

View File

@@ -16,8 +16,6 @@
package org.springframework.cloud.netflix.feign.support;
import static org.springframework.cloud.netflix.feign.support.FeignUtils.getHttpHeaders;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
@@ -25,8 +23,6 @@ import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.Collection;
import lombok.extern.apachecommons.CommonsLog;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.http.HttpHeaders;
@@ -37,6 +33,10 @@ import org.springframework.http.converter.HttpMessageConverter;
import feign.RequestTemplate;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import lombok.extern.apachecommons.CommonsLog;
import static org.springframework.cloud.netflix.feign.support.FeignUtils.getHeaders;
import static org.springframework.cloud.netflix.feign.support.FeignUtils.getHttpHeaders;
/**
* @author Spencer Gibb
@@ -89,6 +89,12 @@ public class SpringEncoder implements Encoder {
catch (IOException ex) {
throw new EncodeException("Error converting request body", ex);
}
// clear headers
request.headers(null);
// converters can modify headers, so update the request
// with the modified headers
request.headers(getHeaders(outputMessage.getHeaders()));
request.body(outputMessage.getOutputStream().toByteArray(),
Charset.forName("UTF-8")); // TODO: set charset
return;
@@ -107,10 +113,10 @@ public class SpringEncoder implements Encoder {
private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
private final RequestTemplate request;
private final HttpHeaders httpHeaders;
private FeignOutputMessage(RequestTemplate request) {
this.request = request;
httpHeaders = getHttpHeaders(request.headers());
}
@Override
@@ -120,7 +126,7 @@ public class SpringEncoder implements Encoder {
@Override
public HttpHeaders getHeaders() {
return getHttpHeaders(this.request.headers());
return this.httpHeaders;
}
public ByteArrayOutputStream getOutputStream() {

View File

@@ -0,0 +1,154 @@
package org.springframework.cloud.netflix.feign.support;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Collection;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatcher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.cloud.netflix.feign.FeignContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractGenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.web.bind.annotation.RestController;
import feign.RequestTemplate;
import lombok.Data;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
/**
* @author Spencer Gibb
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SpringEncoderTests.Application.class)
@WebAppConfiguration
@IntegrationTest({ "server.port=0", "spring.application.name=springencodertest",
"spring.jmx.enabled=true" })
@DirtiesContext
public class SpringEncoderTests {
@Autowired
private FeignContext context;
@Autowired
@Qualifier("myHttpMessageConverter")
private HttpMessageConverter myConverter;
@Test
public void testCustomHttpMessageConverter() {
SpringEncoder encoder = this.context.getInstance("foo", SpringEncoder.class);
assertThat(encoder, is(notNullValue()));
RequestTemplate request = new RequestTemplate();
encoder.encode("hi", MyType.class, request);
Collection<String> contentTypeHeader = request.headers().get("Content-Type");
assertThat("missing content type header", contentTypeHeader, is(notNullValue()));
assertThat("missing content type header", contentTypeHeader.isEmpty(), is(false));
String header = contentTypeHeader.iterator().next();
assertThat("content type header is wrong", header, is("application/mytype"));
}
class MediaTypeMatcher extends ArgumentMatcher<MediaType> {
private MediaType mediaType;
public MediaTypeMatcher(String type, String subtype) {
mediaType = new MediaType(type, subtype);
}
@Override
public boolean matches(Object argument) {
if (argument instanceof MediaType) {
MediaType other = (MediaType) argument;
return mediaType.equals(other);
}
return false;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("MediaTypeMatcher{");
sb.append("mediaType=").append(mediaType);
sb.append('}');
return sb.toString();
}
}
@Data
protected static class MyType {
private String value;
}
protected interface TestClient {
}
@Configuration
@EnableAutoConfiguration
@RestController
protected static class Application implements TestClient {
@Bean
HttpMessageConverter myHttpMessageConverter() {
return new MyHttpMessageConverter();
}
private static class MyHttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
public MyHttpMessageConverter() {
super(new MediaType("application", "mytype"));
}
@Override
protected boolean supports(Class clazz) {
return false;
}
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return true;
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return true;
}
@Override
protected void writeInternal(Object o, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
}
@Override
protected Object readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
public Object read(Type type, Class contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
}
}
}

View File

@@ -86,7 +86,7 @@ public class FeignHttpClientTests {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello getHello();
@RequestMapping(method = RequestMethod.PATCH, value = "/hellop")
@RequestMapping(method = RequestMethod.PATCH, value = "/hellop", consumes = "application/json")
ResponseEntity<Void> patchHello(Hello hello);
}