Some Eclipse Foundation services are deprecated, or will be soon. Please ensure you've read this important communication.

Bug 354014

Summary: Content-Length not passed to wrapped response in GZipFilter
Product: [RT] Jetty Reporter: Mike Pilone <mpilone>
Component: serverAssignee: Michael Gorovoy <mgorovoy>
Status: RESOLVED FIXED QA Contact:
Severity: normal    
Priority: P3 CC: edi.weissmann+eclipse, janb, jesse.mcconnell, jetty-inbox, mgorovoy
Version: unspecified   
Target Milestone: 7.5.x   
Hardware: PC   
OS: Windows Vista   
Whiteboard:
Attachments:
Description Flags
Slightly modified test case with larger test data set.
none
Simple servlet which demonstrates that the results change based on the order of the getOutputStream call. none

Description Mike Pilone CLA 2011-08-05 11:00:35 EDT
Build Identifier: 7.4.5.v20110725

I was getting "Transfer-Encoding: chunked" in my responses from Jetty even though I was setting the Content-Length header. What I finally figured out is that there is a bug/strange interaction with the GZipFilter.
 
The GZipFilter wraps the response object and filters any addHeader calls. If the header is content-length, it stores the length but doesn't pass the header on to the real response. This is fine if the response is to be zipped, but when the content-type header is set, the gzip response wrapper properly determines that the response should not be zipped based on my mime type list. However it never passes on the content-length header to the original response. This causes the HttpGenerator to default to chunked encoding.
 
It looks like line GZipStream tries to be good about passing on the content-length header if there is data in the buffer and it is told not to gzip (around line 252). But if there is no gzip stream created, as is the case around line 299 of GzipResponseWrapper, the header is never passed on. It looks like the GzipStream should always be created but set to doNotGzip during mime-type filtering or the wrapper needs to be smarter about passing on the header when short-cutting the GzipStream.
 
Unfortunately I don't think I can use the GZipFilter until this is fixed because my client (Windows Media Player) won't buffer/live stream a response unless it gets a Content-Length header in the initial response. I don't see any good work around other than trying to limit the filter by pattern in my web.xml to avoid particular files.


Reproducible: Always

Steps to Reproduce:
1. Configure a webapp with a GzipFilter
2. Exclude a mime-type from the filter such as audio/mpeg
3. Create a servlet that sets the Content-Length and Content-Type headers in the response and then writes data to the response output stream
4. Make a request to the servlet with gzip in the Accept header
5. Verify that the response headers include Transfer-Encoding: chunked
Comment 1 Michael Gorovoy CLA 2011-08-05 11:29:15 EDT
Will look into this shortly.
Comment 2 Michael Gorovoy CLA 2011-08-05 12:00:11 EDT
Upon looking over the GzipFilter code, it appears that if the MIME type of the requested content is added to 'exclude' init parameter of the filter, it would not even enter the code you are referring to (see GzipFilter.java, line 129). Please configure this parameter in your application while I'm further investigating this issue.
Comment 3 Michael Gorovoy CLA 2011-08-05 12:48:24 EDT
After looking further into this issue, I was able to confirm that GzipFilter correctly passes Content-Length header. Method GzipResponseWrapper.getOutputStream() (lines 294-307) is called from line 248 of method GzipStream.doNotGzip() (lines 242-261) therefore the output stream is always present when line 252 inside the latter method is processed.

I've committed my test harness that tests for this condition that you could examine here: http://goo.gl/uIcVu. Please feel free to re-open this bug if you are going to be able to create a test harness that demonstrate the issue you are experiencing.
Comment 4 Mike Pilone CLA 2011-08-08 08:56:42 EDT
The problem appears to be related to the size of the response. If the response gets large enough and forces a buffer flush, the problem appears. I modified the original test case to show the issue.

The modified test case uses a simple ContentLengthServlet to keep things extremely simple. It also has a simple loop that generates a much larger block of content, which more accurately represents what happens when a real mp3/mp2 file is retrieved. These files are usually in the 5MB - 30MB range.

Interestingly the test passes if the doGetStreamBeforeContentLength method is used in the test ContentLenghServlet rather than the doGetStreamAfterContentLength. This is because of the way the GZipFilter handles the content-length. If the internal GZipStream is created before the content length header is set, the header is properly passed to the original response. However if the determination of noGzip is done before the GZipStream is created, the content-length header is not passed correctly.

Note: you could probably do this with the DefaultServlet, but I wanted to keep it simple for my own debugging.

-mike
Comment 5 Mike Pilone CLA 2011-08-08 08:57:59 EDT
Created attachment 201075 [details]
Slightly modified test case with larger test data set.
Comment 6 Mike Pilone CLA 2011-08-08 08:58:42 EDT
Created attachment 201076 [details]
Simple servlet which demonstrates that the results change based on the order of the getOutputStream call.
Comment 7 Mike Pilone CLA 2011-08-08 09:03:05 EDT
I looked into using the exclude option. It isn't clearly documented but the actual init parameter is called "excludedAgents" and it is based on UserAgent and not mime-type. This should probably be added to the GzipFilter class docs.

In this particular case it might actually be a decent workaround because I know all the failing requests are coming from Windows Media Player for streaming but in the long run it wouldn't help other clients who aren't getting a Content-Length header.

Thanks for looking into it so quickly.
Comment 8 Michael Gorovoy CLA 2011-08-08 11:16:03 EDT
You are correct, the parameter name is 'excludeAgents' and it takes user-agent values. Sorry for the confusion.

What causes the issue that you are experiencing to occur is that your servlet is setting response headers, either directly or indirectly, *before* it calls Response.getOutputStream(). This effectively disables GzipFilter because the response is already committed by the time GzipResponseWrapper could create a GzipOutputStream. Thus GzipResponseWrapper bails out at line 299.

I'm going to add code to set the content length in wrapped Response object if the wrapper has to bail out. You will need to modify any of your servlets that set request headers before calling Response.getOutputStream() or Response.getWriter() to set them afterwards in order to be able to take full advantage of GzipFilter's capabilities.

-Michael
Comment 9 Michael Gorovoy CLA 2011-08-08 18:31:56 EDT
For the record, the magic content length value is 8192 and it is the size of internal buffer inside GzipResponseWrapper. If the content length is less then the size of the buffer, the decision 'to gzip, or not to gzip' is made at the time the stream is closed. Otherwise, it is made as soon as buffer size is exceeded.

Depending on the sequence of calls made to Response.getOutputStream()/getWriter() as well as Response.setContentType() and Response.setContentLength() the GzipFilter was failing to pass the content length at two different places inside two different classes. I've finally been able to track it all down and fix it for good.
Comment 10 edi weissmann CLA 2012-10-18 11:02:55 EDT
Hi, 

In which jetty version is this bug fixed?
I'm still able to reproduce it in 7.5.4

Thanks,
Eduard
Comment 11 Jesse McConnell CLA 2012-10-18 12:19:14 EDT
going by dates 7.5.4 was out 24 10 2012 so I would think it would be in that release.

did you verify that the comments in 8 and 9 did not apply to your issue?...if yes the feel free to reopen with additional information
Comment 12 Jan Bartel CLA 2012-10-18 17:40:05 EDT
Err, that was a year earlier: 24 10 2011

:)

Jan

(In reply to comment #11)
> going by dates 7.5.4 was out 24 10 2012 so I would think it would be in that
> release.
> 
> did you verify that the comments in 8 and 9 did not apply to your
> issue?...if yes the feel free to reopen with additional information