diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java index 9eb14361..baf274f0 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java @@ -116,84 +116,77 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered { boolean preserveHost = exchange .getAttributeOrDefault(PRESERVE_HOST_HEADER_ATTRIBUTE, false); - Flux responseFlux = this.httpClient.request(method).uri(url) - .send((req, nettyOutbound) -> { - req.headers(httpHeaders); + Flux 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(), diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/GatewayMetricsFilterTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/GatewayMetricsFilterTests.java index 6038bb62..f1cf28ab 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/GatewayMetricsFilterTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/GatewayMetricsFilterTests.java @@ -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 diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/FormIntegrationTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/FormIntegrationTests.java index fbbc20c7..5db4de21 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/FormIntegrationTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/FormIntegrationTests.java @@ -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 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> 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> formData = createMultipartData(); TestRestTemplate rest = new TestRestTemplate(); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.IMAGE_PNG); - - HttpEntity entity = new HttpEntity<>(img, headers); - - MultiValueMap parts = new LinkedMultiValueMap<>(); - parts.add("imgpart", entity); - - ResponseEntity response = rest.postForEntity(baseUri + "/post", parts, + ResponseEntity response = rest.postForEntity(baseUri + "/post", formData, Map.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - Map files = getMap(response.getBody(), "files"); + assertMultipartData(response.getBody()); + } + + private MultiValueMap> 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 files = getMap(responseBody, "files"); assertThat(files).containsKey("imgpart"); String file = (String) files.get("imgpart"); assertThat(file).startsWith("data:").contains(";base64,");