Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
S
spring-cloud-netflix
Project
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
openSource
spring-cloud-netflix
Commits
24209635
Commit
24209635
authored
Apr 12, 2018
by
Bertrand Renuart
Committed by
Ryan Baxter
Apr 12, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Handle GZip response from downstream with an empty body. (#2838)
* Handle GZip response from downstream with an empty body.
parent
196abf2a
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
233 additions
and
76 deletions
+233
-76
SendResponseFilter.java
...k/cloud/netflix/zuul/filters/post/SendResponseFilter.java
+126
-52
SendResponseFilterTests.java
...ud/netflix/zuul/filters/post/SendResponseFilterTests.java
+107
-24
No files found.
spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/post/SendResponseFilter.java
View file @
24209635
...
...
@@ -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
();
}
}
}
spring-cloud-netflix-zuul/src/test/java/org/springframework/cloud/netflix/zuul/filters/post/SendResponseFilterTests.java
View file @
24209635
...
...
@@ -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
();
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment