Commit 24209635 by Bertrand Renuart Committed by Ryan Baxter

Handle GZip response from downstream with an empty body. (#2838)

* Handle GZip response from downstream with an empty body.
parent 196abf2a
......@@ -17,11 +17,14 @@
package org.springframework.cloud.netflix.zuul.filters.post;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.SequenceInputStream;
import java.util.List;
import java.util.Objects;
import java.util.zip.GZIPInputStream;
import javax.servlet.http.HttpServletResponse;
......@@ -60,8 +63,8 @@ public class SendResponseFilter extends ZuulFilter {
@Deprecated
public SendResponseFilter() {
this(new ZuulProperties());
}
this(new ZuulProperties());
}
public SendResponseFilter(ZuulProperties zuulProperties) {
this.zuulProperties = zuulProperties;
......@@ -121,68 +124,40 @@ public class SendResponseFilter extends ZuulFilter {
if (servletResponse.getCharacterEncoding() == null) { // only set if not set
servletResponse.setCharacterEncoding("UTF-8");
}
OutputStream outStream = servletResponse.getOutputStream();
InputStream is = null;
try {
if (RequestContext.getCurrentContext().getResponseBody() != null) {
String body = RequestContext.getCurrentContext().getResponseBody();
writeResponse(
new ByteArrayInputStream(
body.getBytes(servletResponse.getCharacterEncoding())),
outStream);
return;
if (context.getResponseBody() != null) {
String body = context.getResponseBody();
is = new ByteArrayInputStream(
body.getBytes(servletResponse.getCharacterEncoding()));
}
boolean isGzipRequested = false;
final String requestEncoding = context.getRequest()
.getHeader(ZuulHeaders.ACCEPT_ENCODING);
if (requestEncoding != null
&& HTTPRequestUtils.getInstance().isGzipped(requestEncoding)) {
isGzipRequested = true;
}
is = context.getResponseDataStream();
InputStream inputStream = is;
if (is != null) {
if (context.sendZuulResponse()) {
else {
is = context.getResponseDataStream();
if (is!=null && context.getResponseGZipped()) {
// if origin response is gzipped, and client has not requested gzip,
// decompress stream
// before sending to client
// decompress stream before sending to client
// else, stream gzip directly to client
if (context.getResponseGZipped() && !isGzipRequested) {
// If origin tell it's GZipped but the content is ZERO bytes,
// don't try to uncompress
final Long len = context.getOriginContentLength();
if (len == null || len > 0) {
try {
inputStream = new GZIPInputStream(is);
}
catch (java.util.zip.ZipException ex) {
log.debug(
"gzip expected but not "
+ "received assuming unencoded response "
+ RequestContext.getCurrentContext()
.getRequest().getRequestURL()
.toString());
inputStream = is;
}
}
else {
// Already done : inputStream = is;
}
}
else if (context.getResponseGZipped() && isGzipRequested) {
if (isGzipRequested(context)) {
servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, "gzip");
}
writeResponse(inputStream, outStream);
else {
is = handleGzipStream(is);
}
}
}
if (is!=null) {
writeResponse(is, outStream);
}
}
finally {
/**
* We must ensure that the InputStream provided by our upstream pooling mechanism is ALWAYS closed
* even in the case of wrapped streams, which are supplied by pooled sources such as Apache's
* PoolingHttpClientConnectionManager. In that particular case, the underlying HTTP connection will
* be returned back to the connection pool iif either close() is explicitly called, a read
* even in the case of wrapped streams, which are supplied by pooled sources such as Apache's
* PoolingHttpClientConnectionManager. In that particular case, the underlying HTTP connection will
* be returned back to the connection pool iif either close() is explicitly called, a read
* error occurs, or the end of the underlying stream is reached. If, however a write error occurs, we will
* end up leaking a connection from the pool without an explicit close()
*
......@@ -198,8 +173,7 @@ public class SendResponseFilter extends ZuulFilter {
}
try {
Object zuulResponse = RequestContext.getCurrentContext()
.get("zuulResponse");
Object zuulResponse = context.get("zuulResponse");
if (zuulResponse instanceof Closeable) {
((Closeable) zuulResponse).close();
}
......@@ -212,6 +186,47 @@ public class SendResponseFilter extends ZuulFilter {
}
}
protected InputStream handleGzipStream(InputStream in) throws Exception {
// Record bytes read during GZip initialization to allow to rewind the stream if needed
//
RecordingInputStream stream = new RecordingInputStream(in);
try {
return new GZIPInputStream(stream);
}
catch (java.util.zip.ZipException | java.io.EOFException ex) {
if (stream.getBytesRead()==0) {
// stream was empty, return the original "empty" stream
return in;
}
else {
// reset the stream and assume an unencoded response
log.warn(
"gzip response expected but failed to read gzip headers, assuming unencoded response for request "
+ RequestContext.getCurrentContext()
.getRequest().getRequestURL()
.toString());
stream.reset();
return stream;
}
}
finally {
stream.stopRecording();
}
}
protected boolean isGzipRequested(RequestContext context) {
final String requestEncoding = context.getRequest()
.getHeader(ZuulHeaders.ACCEPT_ENCODING);
return requestEncoding != null
&& HTTPRequestUtils.getInstance().isGzipped(requestEncoding);
}
private void writeResponse(InputStream zin, OutputStream out) throws Exception {
byte[] bytes = buffers.get();
int bytesRead = -1;
......@@ -276,4 +291,63 @@ public class SendResponseFilter extends ZuulFilter {
// Forward it in all other cases
return true;
}
/**
* InputStream recording bytes read to allow for a reset() until recording is stopped.
*/
private static class RecordingInputStream extends InputStream {
private InputStream delegate;
private ByteArrayOutputStream buffer = new ByteArrayOutputStream();
public RecordingInputStream(InputStream delegate) {
super();
this.delegate = Objects.requireNonNull(delegate);
}
@Override
public int read() throws IOException {
int read = delegate.read();
if (buffer!=null && read!=-1) {
buffer.write(read);
}
return read;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int read = delegate.read(b, off, len);
if (buffer!=null && read!=-1) {
buffer.write(b, off, read);
}
return read;
}
public void reset() {
if (buffer==null) {
throw new IllegalStateException("Stream is not recording");
}
this.delegate = new SequenceInputStream(new ByteArrayInputStream(buffer.toByteArray()), delegate);
this.buffer = new ByteArrayOutputStream();
}
public int getBytesRead() {
return (buffer==null)?-1:buffer.size();
}
public void stopRecording() {
this.buffer = null;
}
@Override
public void close() throws IOException {
this.delegate.close();
}
}
}
......@@ -16,11 +16,31 @@
package org.springframework.cloud.netflix.zuul.filters.post;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.X_ZUUL_DEBUG_HEADER;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
......@@ -30,7 +50,6 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
......@@ -40,20 +59,6 @@ import com.netflix.zuul.constants.ZuulHeaders;
import com.netflix.zuul.context.Debug;
import com.netflix.zuul.context.RequestContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.X_ZUUL_DEBUG_HEADER;
/**
* @author Spencer Gibb
*/
......@@ -62,7 +67,10 @@ public class SendResponseFilterTests {
@Before
public void setTestRequestcontext() {
RequestContext context = new RequestContext();
context.setRequest(new MockHttpServletRequest());
context.setResponse(new MockHttpServletResponse());
context.setResponseGZipped(false);
RequestContext.testSetCurrentContext(context);
}
......@@ -123,41 +131,107 @@ public class SendResponseFilterTests {
}
/*
* GZip requested and GZip response -> Content-Length forwarded asis
* GZip requested and GZip response -> Content-Length forwarded asis, response compressed
*/
@Test
public void runWithOriginContentLength_gzipRequested_gzipResponse() throws Exception {
ZuulProperties properties = new ZuulProperties();
properties.setSetContentLength(true);
SendResponseFilter filter = createFilter(properties, "hello", "UTF-8", new MockHttpServletResponse(), true);
RequestContext.getCurrentContext().setOriginContentLength(6L); // for test
SendResponseFilter filter = new SendResponseFilter(properties);
byte[] gzipData = gzipData("hello");
RequestContext.getCurrentContext().setOriginContentLength((long) gzipData.length); // for test
RequestContext.getCurrentContext().setResponseGZipped(true);
RequestContext.getCurrentContext().setResponseDataStream( new ByteArrayInputStream(gzipData) );
((MockHttpServletRequest) RequestContext.getCurrentContext().getRequest()).addHeader(ZuulHeaders.ACCEPT_ENCODING, "gzip");
filter.run();
String contentLength = RequestContext.getCurrentContext().getResponse().getHeader("Content-Length");
assertThat("wrong origin content length", contentLength, equalTo("6"));
MockHttpServletResponse response = (MockHttpServletResponse) RequestContext.getCurrentContext().getResponse();
assertThat(response.getHeader("Content-Length")).isEqualTo(Integer.toString(gzipData.length));
assertThat(response.getHeader("Content-Encoding")).isEqualTo("gzip");
assertThat(response.getContentAsByteArray()).isEqualTo(gzipData);
BufferedReader reader = new BufferedReader(new InputStreamReader(new GZIPInputStream(new ByteArrayInputStream(response.getContentAsByteArray()))));
assertThat(reader.readLine()).isEqualTo("hello");
}
/*
* GZip NOT requested and GZip response -> Content-Length discarded
* GZip NOT requested and GZip response -> Content-Length discarded and response uncompressed
*/
@Test
public void runWithOriginContentLength_gzipNotRequested_gzipResponse() throws Exception {
ZuulProperties properties = new ZuulProperties();
properties.setSetContentLength(true);
SendResponseFilter filter = createFilter(properties, "hello", "UTF-8", new MockHttpServletResponse(), true);
RequestContext.getCurrentContext().setOriginContentLength(6L); // for test
SendResponseFilter filter = new SendResponseFilter(properties);
byte[] gzipData = gzipData("hello");
RequestContext.getCurrentContext().setOriginContentLength((long) gzipData.length); // for test
RequestContext.getCurrentContext().setResponseGZipped(true);
RequestContext.getCurrentContext().setResponseDataStream( new ByteArrayInputStream(gzipData) );
filter.run();
MockHttpServletResponse response = (MockHttpServletResponse) RequestContext.getCurrentContext().getResponse();
assertThat(response.getHeader("Content-Length")).isNull();
assertThat(response.getHeader("Content-Encoding")).isNull();
assertThat("wrong content", response.getContentAsString(), equalTo("hello"));
}
/*
* Origin sends a non gzip response with Content-Encoding: gzip
* Request does not support GZIP -> filter fails to uncompress and send stream "asis". Content-Length is NOT preserved.
*/
@Test
public void invalidGzipResponseFromOrigin() throws Exception {
ZuulProperties properties = new ZuulProperties();
properties.setSetContentLength(true);
SendResponseFilter filter = new SendResponseFilter(properties);
byte[] gzipData = "hello".getBytes();
RequestContext.getCurrentContext().setOriginContentLength((long) gzipData.length); // for test
RequestContext.getCurrentContext().setResponseGZipped(true); // say it is GZipped although not the case
RequestContext.getCurrentContext().setResponseDataStream( new ByteArrayInputStream(gzipData) );
filter.run();
MockHttpServletResponse response = (MockHttpServletResponse) RequestContext.getCurrentContext().getResponse();
assertThat(response.getHeader("Content-Length")).isNull();
assertThat(response.getHeader("Content-Encoding")).isNull();
assertThat("wrong content", response.getContentAsString(), equalTo("hello")); // response sent "asis"
}
/*
* Empty response from origin with Content-Encoding: gzip
* Request does not support GZIP -> filter should not fail in decoding the *empty* response stream
*/
@Test
public void emptyGzipResponseFromOrigin() throws Exception {
ZuulProperties properties = new ZuulProperties();
properties.setSetContentLength(true);
SendResponseFilter filter = new SendResponseFilter(properties);
byte[] gzipData = new byte[] {};
RequestContext.getCurrentContext().setResponseGZipped(true);
RequestContext.getCurrentContext().setResponseDataStream( new ByteArrayInputStream(gzipData) );
filter.run();
assertThat(RequestContext.getCurrentContext().getResponse().getHeader("Content-Length")).isNull();
MockHttpServletResponse response = (MockHttpServletResponse) RequestContext.getCurrentContext().getResponse();
assertThat(response.getHeader("Content-Length")).isNull();
assertThat(response.getHeader("Content-Encoding")).isNull();
assertThat(response.getContentAsByteArray(), equalTo(gzipData));
}
@Test
public void closeResponseOutputStreamError() throws Exception {
HttpServletResponse response = mock(HttpServletResponse.class);
......@@ -250,4 +324,13 @@ public class SendResponseFilterTests {
return filter;
}
private byte[] gzipData(String content) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
PrintWriter gzip = new PrintWriter(new GZIPOutputStream(bos));
gzip.print(content);
gzip.flush();
gzip.close();
return bos.toByteArray();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment