Merge branch '2.1.x'
This commit is contained in:
@@ -116,84 +116,77 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered {
|
||||
boolean preserveHost = exchange
|
||||
.getAttributeOrDefault(PRESERVE_HOST_HEADER_ATTRIBUTE, false);
|
||||
|
||||
Flux<HttpClientResponse> responseFlux = this.httpClient.request(method).uri(url)
|
||||
.send((req, nettyOutbound) -> {
|
||||
req.headers(httpHeaders);
|
||||
Flux<HttpClientResponse> responseFlux = this.httpClient.headers(headers -> {
|
||||
headers.add(httpHeaders);
|
||||
if (preserveHost) {
|
||||
String host = request.getHeaders().getFirst(HttpHeaders.HOST);
|
||||
headers.add(HttpHeaders.HOST, host);
|
||||
}
|
||||
}).request(method).uri(url).send((req, nettyOutbound) -> {
|
||||
if (log.isTraceEnabled()) {
|
||||
nettyOutbound.withConnection(connection -> log.trace(
|
||||
"outbound route: " + connection.channel().id().asShortText()
|
||||
+ ", inbound: " + exchange.getLogPrefix()));
|
||||
}
|
||||
return nettyOutbound.send(request.getBody()
|
||||
.map(dataBuffer -> ((NettyDataBuffer) dataBuffer).getNativeBuffer()));
|
||||
}).responseConnection((res, connection) -> {
|
||||
|
||||
if (preserveHost) {
|
||||
String host = request.getHeaders().getFirst(HttpHeaders.HOST);
|
||||
req.header(HttpHeaders.HOST, host);
|
||||
}
|
||||
if (log.isTraceEnabled()) {
|
||||
nettyOutbound
|
||||
.withConnection(connection -> log.trace("outbound route: "
|
||||
+ connection.channel().id().asShortText()
|
||||
+ ", inbound: " + exchange.getLogPrefix()));
|
||||
}
|
||||
return nettyOutbound.send(request.getBody()
|
||||
.map(dataBuffer -> ((NettyDataBuffer) dataBuffer)
|
||||
.getNativeBuffer()));
|
||||
}).responseConnection((res, connection) -> {
|
||||
// Defer committing the response until all route filters have run
|
||||
// Put client response as ServerWebExchange attribute and write
|
||||
// response later NettyWriteResponseFilter
|
||||
exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res);
|
||||
exchange.getAttributes().put(CLIENT_RESPONSE_CONN_ATTR, connection);
|
||||
|
||||
// Defer committing the response until all route filters have run
|
||||
// Put client response as ServerWebExchange attribute and write
|
||||
// response later NettyWriteResponseFilter
|
||||
exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res);
|
||||
exchange.getAttributes().put(CLIENT_RESPONSE_CONN_ATTR, connection);
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
// put headers and status so filters can modify the response
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
// put headers and status so filters can modify the response
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
res.responseHeaders()
|
||||
.forEach(entry -> headers.add(entry.getKey(), entry.getValue()));
|
||||
|
||||
res.responseHeaders().forEach(
|
||||
entry -> headers.add(entry.getKey(), entry.getValue()));
|
||||
String contentTypeValue = headers.getFirst(HttpHeaders.CONTENT_TYPE);
|
||||
if (StringUtils.hasLength(contentTypeValue)) {
|
||||
exchange.getAttributes().put(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR,
|
||||
contentTypeValue);
|
||||
}
|
||||
|
||||
String contentTypeValue = headers.getFirst(HttpHeaders.CONTENT_TYPE);
|
||||
if (StringUtils.hasLength(contentTypeValue)) {
|
||||
exchange.getAttributes().put(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR,
|
||||
contentTypeValue);
|
||||
}
|
||||
HttpStatus status = HttpStatus.resolve(res.status().code());
|
||||
if (status != null) {
|
||||
response.setStatusCode(status);
|
||||
}
|
||||
else if (response instanceof AbstractServerHttpResponse) {
|
||||
// https://jira.spring.io/browse/SPR-16748
|
||||
((AbstractServerHttpResponse) response)
|
||||
.setStatusCodeValue(res.status().code());
|
||||
}
|
||||
else {
|
||||
// TODO: log warning here, not throw error?
|
||||
throw new IllegalStateException("Unable to set status code on response: "
|
||||
+ res.status().code() + ", " + response.getClass());
|
||||
}
|
||||
|
||||
HttpStatus status = HttpStatus.resolve(res.status().code());
|
||||
if (status != null) {
|
||||
response.setStatusCode(status);
|
||||
}
|
||||
else if (response instanceof AbstractServerHttpResponse) {
|
||||
// https://jira.spring.io/browse/SPR-16748
|
||||
((AbstractServerHttpResponse) response)
|
||||
.setStatusCodeValue(res.status().code());
|
||||
}
|
||||
else {
|
||||
// TODO: log warning here, not throw error?
|
||||
throw new IllegalStateException(
|
||||
"Unable to set status code on response: "
|
||||
+ res.status().code() + ", "
|
||||
+ response.getClass());
|
||||
}
|
||||
// make sure headers filters run after setting status so it is
|
||||
// available in response
|
||||
HttpHeaders filteredResponseHeaders = HttpHeadersFilter
|
||||
.filter(getHeadersFilters(), headers, exchange, Type.RESPONSE);
|
||||
|
||||
// make sure headers filters run after setting status so it is
|
||||
// available in response
|
||||
HttpHeaders filteredResponseHeaders = HttpHeadersFilter.filter(
|
||||
getHeadersFilters(), headers, exchange, Type.RESPONSE);
|
||||
if (!filteredResponseHeaders.containsKey(HttpHeaders.TRANSFER_ENCODING)
|
||||
&& filteredResponseHeaders.containsKey(HttpHeaders.CONTENT_LENGTH)) {
|
||||
// It is not valid to have both the transfer-encoding header and
|
||||
// the content-length header.
|
||||
// Remove the transfer-encoding header in the response if the
|
||||
// content-length header is present.
|
||||
response.getHeaders().remove(HttpHeaders.TRANSFER_ENCODING);
|
||||
}
|
||||
|
||||
if (!filteredResponseHeaders
|
||||
.containsKey(HttpHeaders.TRANSFER_ENCODING)
|
||||
&& filteredResponseHeaders
|
||||
.containsKey(HttpHeaders.CONTENT_LENGTH)) {
|
||||
// It is not valid to have both the transfer-encoding header and
|
||||
// the content-length header.
|
||||
// Remove the transfer-encoding header in the response if the
|
||||
// content-length header is present.
|
||||
response.getHeaders().remove(HttpHeaders.TRANSFER_ENCODING);
|
||||
}
|
||||
exchange.getAttributes().put(CLIENT_RESPONSE_HEADER_NAMES,
|
||||
filteredResponseHeaders.keySet());
|
||||
|
||||
exchange.getAttributes().put(CLIENT_RESPONSE_HEADER_NAMES,
|
||||
filteredResponseHeaders.keySet());
|
||||
response.getHeaders().putAll(filteredResponseHeaders);
|
||||
|
||||
response.getHeaders().putAll(filteredResponseHeaders);
|
||||
|
||||
return Mono.just(res);
|
||||
});
|
||||
return Mono.just(res);
|
||||
});
|
||||
|
||||
if (properties.getResponseTimeout() != null) {
|
||||
responseFlux = responseFlux.timeout(properties.getResponseTimeout(),
|
||||
|
||||
@@ -99,8 +99,12 @@ public class GatewayMetricsFilterTests extends BaseWebClientTests {
|
||||
}
|
||||
|
||||
private void assertMetricsContainsTag(String tagKey, String tagValue) {
|
||||
// @formatter:off
|
||||
assertThat(this.meterRegistry.get(REQUEST_METRICS_NAME).tag(tagKey, tagValue)
|
||||
.timer().count()).isEqualTo(1);
|
||||
.timer().count())
|
||||
.as("Wrong value for metric %s: %s", tagKey, tagValue)
|
||||
.isGreaterThanOrEqualTo(1);
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
|
||||
@@ -29,10 +29,10 @@ import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.MultipartBodyBuilder;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
@@ -43,6 +43,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
|
||||
import static org.springframework.cloud.gateway.test.TestUtils.getMap;
|
||||
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED;
|
||||
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(webEnvironment = RANDOM_PORT)
|
||||
@@ -59,35 +60,55 @@ public class FormIntegrationTests extends BaseWebClientTests {
|
||||
formData.add("foo", "bar");
|
||||
formData.add("baz", "bam");
|
||||
|
||||
// @formatter:off
|
||||
testClient.post().uri("/post").contentType(FORM_URL_ENCODED_CONTENT_TYPE)
|
||||
.body(BodyInserters.fromFormData(formData)).exchange().expectStatus()
|
||||
.isOk().expectBody(Map.class).consumeWith(result -> {
|
||||
.body(BodyInserters.fromFormData(formData))
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(Map.class).consumeWith(result -> {
|
||||
Map map = result.getResponseBody();
|
||||
Map<String, Object> form = getMap(map, "form");
|
||||
assertThat(form).containsEntry("foo", "bar");
|
||||
assertThat(form).containsEntry("baz", "bam");
|
||||
});
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipartFormDataWorks() {
|
||||
ClassPathResource img = new ClassPathResource("1x1.png");
|
||||
public void multipartFormDataWorksWebClient() {
|
||||
MultiValueMap<String, HttpEntity<?>> formData = createMultipartData();
|
||||
|
||||
// @formatter:off
|
||||
testClient.post().uri("/post").contentType(MULTIPART_FORM_DATA)
|
||||
.syncBody(formData)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(Map.class)
|
||||
.consumeWith(result -> assertMultipartData(result.getResponseBody()));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipartFormDataWorksRestTemplate() {
|
||||
MultiValueMap<String, HttpEntity<?>> formData = createMultipartData();
|
||||
TestRestTemplate rest = new TestRestTemplate();
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.IMAGE_PNG);
|
||||
|
||||
HttpEntity<ClassPathResource> entity = new HttpEntity<>(img, headers);
|
||||
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
parts.add("imgpart", entity);
|
||||
|
||||
ResponseEntity<Map> response = rest.postForEntity(baseUri + "/post", parts,
|
||||
ResponseEntity<Map> response = rest.postForEntity(baseUri + "/post", formData,
|
||||
Map.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
Map<String, Object> files = getMap(response.getBody(), "files");
|
||||
assertMultipartData(response.getBody());
|
||||
}
|
||||
|
||||
private MultiValueMap<String, HttpEntity<?>> createMultipartData() {
|
||||
ClassPathResource part = new ClassPathResource("1x1.png");
|
||||
MultipartBodyBuilder builder = new MultipartBodyBuilder();
|
||||
builder.part("imgpart", part, MediaType.IMAGE_PNG);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private void assertMultipartData(Map responseBody) {
|
||||
Map<String, Object> files = getMap(responseBody, "files");
|
||||
assertThat(files).containsKey("imgpart");
|
||||
String file = (String) files.get("imgpart");
|
||||
assertThat(file).startsWith("data:").contains(";base64,");
|
||||
|
||||
Reference in New Issue
Block a user