diff --git a/spring-cloud-gateway-server-webmvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/FormFilter.java b/spring-cloud-gateway-server-webmvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/FormFilter.java index 85218519f..d92fa66cb 100644 --- a/spring-cloud-gateway-server-webmvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/FormFilter.java +++ b/spring-cloud-gateway-server-webmvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/FormFilter.java @@ -44,6 +44,7 @@ import org.springframework.cloud.gateway.server.mvc.common.MvcUtils; import org.springframework.core.Ordered; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.lang.Nullable; @@ -55,6 +56,7 @@ * parameters and form parameters the same, but a proxy should not care. * * @author Spencer Gibb + * @author Hutiefang Hu */ @SuppressWarnings("unchecked") public class FormFilter implements Filter, Ordered { @@ -141,9 +143,10 @@ static HttpServletRequest getRequestWithBodyFromRequestParameters(HttpServletReq } writer.flush(); + byte[] body = bos.toByteArray(); ByteArrayServletInputStream servletInputStream = new ByteArrayServletInputStream( - new ByteArrayInputStream(bos.toByteArray())); - return new FormContentRequestWrapper(request, queryParams) { + new ByteArrayInputStream(body)); + return new FormContentRequestWrapper(request, queryParams, body.length) { @Override public ServletInputStream getInputStream() throws IOException { return servletInputStream; @@ -186,9 +189,50 @@ private static class FormContentRequestWrapper extends HttpServletRequestWrapper private final MultiValueMap queryParams; - FormContentRequestWrapper(HttpServletRequest request, MultiValueMap params) { + private final int contentLength; + + private final boolean contentLengthHeaderPresent; + + FormContentRequestWrapper(HttpServletRequest request, MultiValueMap params, int contentLength) { super(request); this.queryParams = params; + this.contentLength = contentLength; + this.contentLengthHeaderPresent = request.getHeader(HttpHeaders.CONTENT_LENGTH) != null; + } + + @Override + public int getContentLength() { + return this.contentLength; + } + + @Override + public long getContentLengthLong() { + return this.contentLength; + } + + @Override + @Nullable + public String getHeader(String name) { + if (this.contentLengthHeaderPresent && HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + return String.valueOf(this.contentLength); + } + return super.getHeader(name); + } + + @Override + public Enumeration getHeaders(String name) { + if (this.contentLengthHeaderPresent && HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + return Collections.enumeration(List.of(String.valueOf(this.contentLength))); + } + return super.getHeaders(name); + } + + @Override + public int getIntHeader(String name) { + if (this.contentLengthHeaderPresent && HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + return this.contentLength; + } + return super.getIntHeader(name); } @Override diff --git a/spring-cloud-gateway-server-webmvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/FormFilterTests.java b/spring-cloud-gateway-server-webmvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/FormFilterTests.java index f64ca9c9d..99aeaacbe 100644 --- a/spring-cloud-gateway-server-webmvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/FormFilterTests.java +++ b/spring-cloud-gateway-server-webmvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/FormFilterTests.java @@ -21,6 +21,7 @@ import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; import java.util.Enumeration; import java.util.List; @@ -33,6 +34,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mockito; +import org.springframework.http.HttpHeaders; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.util.LinkedMultiValueMap; @@ -45,9 +47,61 @@ /** * @author shawyeok + * @author Hutiefang Hu */ class FormFilterTests { + @Test + void updateContentLengthAfterRebuildingFormBody() throws ServletException, IOException { + byte[] originalBody = "formArg=!".getBytes(StandardCharsets.UTF_8); + MockHttpServletRequest request = MockMvcRequestBuilders.post(URI.create("http://localhost/test")) + .contentType("application/x-www-form-urlencoded") + .header(HttpHeaders.CONTENT_LENGTH, originalBody.length) + .content(originalBody) + .buildRequest(null); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + FilterChain chain = Mockito.mock(FilterChain.class); + + new FormFilter().doFilter(request, response, chain); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ServletRequest.class); + verify(chain).doFilter(captor.capture(), Mockito.eq(response)); + HttpServletRequest wrappedRequest = (HttpServletRequest) captor.getValue(); + byte[] rebuiltBody = "formArg=%21".getBytes(StandardCharsets.UTF_8); + assertThat(StreamUtils.copyToByteArray(wrappedRequest.getInputStream())).isEqualTo(rebuiltBody); + assertThat(wrappedRequest.getContentLength()).isEqualTo(rebuiltBody.length); + assertThat(wrappedRequest.getContentLengthLong()).isEqualTo(rebuiltBody.length); + assertThat(wrappedRequest.getHeader(HttpHeaders.CONTENT_LENGTH)).isEqualTo(String.valueOf(rebuiltBody.length)); + assertThat(Collections.list(wrappedRequest.getHeaders(HttpHeaders.CONTENT_LENGTH))) + .containsExactly(String.valueOf(rebuiltBody.length)); + assertThat(wrappedRequest.getIntHeader("content-length")).isEqualTo(rebuiltBody.length); + assertThat(Collections.list(wrappedRequest.getHeaderNames())).contains(HttpHeaders.CONTENT_LENGTH); + } + + @Test + void preserveMissingContentLengthHeaderAfterRebuildingFormBody() throws ServletException, IOException { + MockHttpServletRequest request = MockMvcRequestBuilders.post(URI.create("http://localhost/test")) + .contentType("application/x-www-form-urlencoded") + .content("formArg=!") + .buildRequest(null); + request.removeHeader(HttpHeaders.CONTENT_LENGTH); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + FilterChain chain = Mockito.mock(FilterChain.class); + + new FormFilter().doFilter(request, response, chain); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ServletRequest.class); + verify(chain).doFilter(captor.capture(), Mockito.eq(response)); + HttpServletRequest wrappedRequest = (HttpServletRequest) captor.getValue(); + int rebuiltContentLength = "formArg=%21".getBytes(StandardCharsets.UTF_8).length; + assertThat(wrappedRequest.getContentLength()).isEqualTo(rebuiltContentLength); + assertThat(wrappedRequest.getContentLengthLong()).isEqualTo(rebuiltContentLength); + assertThat(wrappedRequest.getHeader("content-length")).isNull(); + assertThat(Collections.list(wrappedRequest.getHeaders("content-length"))).isEmpty(); + assertThat(wrappedRequest.getIntHeader("content-length")).isEqualTo(-1); + assertThat(Collections.list(wrappedRequest.getHeaderNames())).doesNotContain(HttpHeaders.CONTENT_LENGTH); + } + @Test void hideFormParameterFromParameterMap() throws ServletException, IOException { FormFilter filter = new FormFilter();