Buffering Response Handler #786
This commit is contained in:
parent
c9842c7794
commit
ffb52a948b
|
@ -0,0 +1,322 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.server.handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.MimeTypes;
|
||||
import org.eclipse.jetty.http.pathmap.PathSpecSet;
|
||||
import org.eclipse.jetty.server.HttpChannel;
|
||||
import org.eclipse.jetty.server.HttpOutput;
|
||||
import org.eclipse.jetty.server.HttpOutput.Interceptor;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.IncludeExclude;
|
||||
import org.eclipse.jetty.util.IteratingCallback;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.URIUtil;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
/**
|
||||
* Buffered Response Handler
|
||||
* <p>
|
||||
* A Handler that can apply a {@link org.eclipse.jetty.server.HttpOutput.Interceptor}
|
||||
* mechanism to buffer the entire response content until the output is closed.
|
||||
* This allows the commit to be delayed until the response is complete and thus
|
||||
* headers and response status can be changed while writing the body.
|
||||
* <p>
|
||||
* Note that the decision to buffer is influenced by the headers and status at the
|
||||
* first write, and thus subsequent changes to those headers will not influence the
|
||||
* decision to buffer or not.
|
||||
* <p>
|
||||
* Note also that there are no memory limits to the size of the buffer, thus
|
||||
* this handler can represent an unbounded memory commitment if the content
|
||||
* generated can also be unbounded.
|
||||
* </p>
|
||||
*/
|
||||
public class BufferedResponseHandler extends HandlerWrapper
|
||||
{
|
||||
static final Logger LOG = Log.getLogger(BufferedResponseHandler.class);
|
||||
|
||||
private final IncludeExclude<String> _methods = new IncludeExclude<>();
|
||||
private final IncludeExclude<String> _paths = new IncludeExclude<>(PathSpecSet.class);
|
||||
private final IncludeExclude<String> _mimeTypes = new IncludeExclude<>();
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public BufferedResponseHandler()
|
||||
{
|
||||
// include only GET requests
|
||||
|
||||
_methods.include(HttpMethod.GET.asString());
|
||||
// Exclude images, aduio and video from buffering
|
||||
for (String type:MimeTypes.getKnownMimeTypes())
|
||||
{
|
||||
if (type.startsWith("image/")||
|
||||
type.startsWith("audio/")||
|
||||
type.startsWith("video/"))
|
||||
_mimeTypes.exclude(type);
|
||||
}
|
||||
LOG.debug("{} mime types {}",this,_mimeTypes);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public IncludeExclude<String> getMethodIncludeExclude()
|
||||
{
|
||||
return _methods;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public IncludeExclude<String> getPathIncludeExclude()
|
||||
{
|
||||
return _paths;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public IncludeExclude<String> getMimeIncludeExclude()
|
||||
{
|
||||
return _mimeTypes;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
* @see org.eclipse.jetty.server.handler.HandlerWrapper#handle(java.lang.String, org.eclipse.jetty.server.Request, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
|
||||
*/
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||
{
|
||||
ServletContext context = baseRequest.getServletContext();
|
||||
String path = context==null?baseRequest.getRequestURI():URIUtil.addPaths(baseRequest.getServletPath(),baseRequest.getPathInfo());
|
||||
LOG.debug("{} handle {} in {}",this,baseRequest,context);
|
||||
|
||||
HttpOutput out = baseRequest.getResponse().getHttpOutput();
|
||||
|
||||
// Are we already being gzipped?
|
||||
HttpOutput.Interceptor interceptor = out.getInterceptor();
|
||||
while (interceptor!=null)
|
||||
{
|
||||
if (interceptor instanceof BufferedInterceptor)
|
||||
{
|
||||
LOG.debug("{} already intercepting {}",this,request);
|
||||
_handler.handle(target,baseRequest, request, response);
|
||||
return;
|
||||
}
|
||||
interceptor=interceptor.getNextInterceptor();
|
||||
}
|
||||
|
||||
// If not a supported method - no Vary because no matter what client, this URI is always excluded
|
||||
if (!_methods.matches(baseRequest.getMethod()))
|
||||
{
|
||||
LOG.debug("{} excluded by method {}",this,request);
|
||||
_handler.handle(target,baseRequest, request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// If not a supported URI- no Vary because no matter what client, this URI is always excluded
|
||||
// Use pathInfo because this is be
|
||||
if (!isPathBufferable(path))
|
||||
{
|
||||
LOG.debug("{} excluded by path {}",this,request);
|
||||
_handler.handle(target,baseRequest, request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the mime type is known from the path, then apply mime type filtering
|
||||
String mimeType = context==null?null:context.getMimeType(path);
|
||||
if (mimeType!=null)
|
||||
{
|
||||
mimeType = MimeTypes.getContentTypeWithoutCharset(mimeType);
|
||||
if (!isMimeTypeBufferable(mimeType))
|
||||
{
|
||||
LOG.debug("{} excluded by path suffix mime type {}",this,request);
|
||||
// handle normally without setting vary header
|
||||
_handler.handle(target,baseRequest, request, response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// install interceptor and handle
|
||||
out.setInterceptor(new BufferedInterceptor(baseRequest.getHttpChannel(),out.getInterceptor()));
|
||||
|
||||
if (_handler!=null)
|
||||
_handler.handle(target,baseRequest, request, response);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
protected boolean isMimeTypeBufferable(String mimetype)
|
||||
{
|
||||
return _mimeTypes.matches(mimetype);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
protected boolean isPathBufferable(String requestURI)
|
||||
{
|
||||
if (requestURI == null)
|
||||
return true;
|
||||
|
||||
return _paths.matches(requestURI);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/* ------------------------------------------------------------ */
|
||||
/* ------------------------------------------------------------ */
|
||||
private class BufferedInterceptor implements HttpOutput.Interceptor
|
||||
{
|
||||
final Interceptor _next;
|
||||
final HttpChannel _channel;
|
||||
final Queue<ByteBuffer> _buffers=new ConcurrentLinkedQueue<>();
|
||||
Boolean _aggregating;
|
||||
ByteBuffer _aggregate;
|
||||
|
||||
public BufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
|
||||
{
|
||||
_next=interceptor;
|
||||
_channel=httpChannel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(ByteBuffer content, boolean last, Callback callback)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("{} write last={} {}",this,last,BufferUtil.toDetailString(content));
|
||||
// if we are not committed, have to decide if we should aggregate or not
|
||||
if (_aggregating==null)
|
||||
{
|
||||
Response response = _channel.getResponse();
|
||||
int sc = response.getStatus();
|
||||
if (sc>0 && (sc<200 || sc==204 || sc==205 || sc>=300))
|
||||
_aggregating=Boolean.FALSE; // No body
|
||||
else
|
||||
{
|
||||
String ct = response.getContentType();
|
||||
if (ct==null)
|
||||
_aggregating=Boolean.TRUE;
|
||||
else
|
||||
{
|
||||
ct=MimeTypes.getContentTypeWithoutCharset(ct);
|
||||
_aggregating=isMimeTypeBufferable(StringUtil.asciiToLowerCase(ct));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we are not aggregating, then handle normally
|
||||
if (!_aggregating.booleanValue())
|
||||
{
|
||||
getNextInterceptor().write(content,last,callback);
|
||||
return;
|
||||
}
|
||||
|
||||
// If last
|
||||
if (last)
|
||||
{
|
||||
// Add the current content to the buffer list without a copy
|
||||
if (BufferUtil.length(content)>0)
|
||||
_buffers.add(content);
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("{} committing {}",this,_buffers.size());
|
||||
commit(_buffers,callback);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("{} aggregating",this);
|
||||
|
||||
// Aggregate the content into buffer chain
|
||||
while (BufferUtil.hasContent(content))
|
||||
{
|
||||
// Do we need a new aggregate buffer
|
||||
if (BufferUtil.space(_aggregate)==0)
|
||||
{
|
||||
int size = Math.max(_channel.getHttpConfiguration().getOutputBufferSize(),BufferUtil.length(content));
|
||||
_aggregate=BufferUtil.allocate(size); // TODO use a buffer pool
|
||||
_buffers.add(_aggregate);
|
||||
}
|
||||
|
||||
BufferUtil.append(_aggregate,content);
|
||||
}
|
||||
callback.succeeded();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Interceptor getNextInterceptor()
|
||||
{
|
||||
return _next;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOptimizedForDirectBuffers()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void commit(Queue<ByteBuffer> buffers, Callback callback)
|
||||
{
|
||||
// If only 1 buffer
|
||||
if (_buffers.size()==1)
|
||||
// just flush it with the last callback
|
||||
getNextInterceptor().write(_buffers.remove(),true,callback);
|
||||
else
|
||||
{
|
||||
// Create an iterating callback to do the writing
|
||||
IteratingCallback icb = new IteratingCallback()
|
||||
{
|
||||
@Override
|
||||
protected Action process() throws Exception
|
||||
{
|
||||
ByteBuffer buffer = _buffers.poll();
|
||||
if (buffer==null)
|
||||
return Action.SUCCEEDED;
|
||||
|
||||
getNextInterceptor().write(buffer,_buffers.isEmpty(),this);
|
||||
return Action.SCHEDULED;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCompleteSuccess()
|
||||
{
|
||||
// Signal last callback
|
||||
callback.succeeded();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCompleteFailure(Throwable cause)
|
||||
{
|
||||
// Signal last callback
|
||||
callback.failed(cause);
|
||||
}
|
||||
};
|
||||
icb.iterate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,239 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.server.handler;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.HttpConnectionFactory;
|
||||
import org.eclipse.jetty.server.LocalConnector;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Resource Handler test
|
||||
*
|
||||
* TODO: increase the testing going on here
|
||||
*/
|
||||
public class BufferedResponseHandlerTest
|
||||
{
|
||||
private static Server _server;
|
||||
private static HttpConfiguration _config;
|
||||
private static LocalConnector _local;
|
||||
private static ContextHandler _contextHandler;
|
||||
private static BufferedResponseHandler _bufferedHandler;
|
||||
private static TestHandler _test;
|
||||
|
||||
@BeforeClass
|
||||
public static void setUp() throws Exception
|
||||
{
|
||||
_server = new Server();
|
||||
_config = new HttpConfiguration();
|
||||
_config.setOutputBufferSize(1024);
|
||||
_config.setOutputAggregationSize(256);
|
||||
_local = new LocalConnector(_server,new HttpConnectionFactory(_config));
|
||||
_server.addConnector(_local);
|
||||
|
||||
_bufferedHandler = new BufferedResponseHandler();
|
||||
_bufferedHandler.getPathIncludeExclude().include("/include/*");
|
||||
_bufferedHandler.getPathIncludeExclude().exclude("*.exclude");
|
||||
_bufferedHandler.getMimeIncludeExclude().exclude("text/excluded");
|
||||
_bufferedHandler.setHandler(_test=new TestHandler());
|
||||
|
||||
_contextHandler = new ContextHandler("/ctx");
|
||||
_contextHandler.setHandler(_bufferedHandler);
|
||||
|
||||
_server.setHandler(_contextHandler);
|
||||
_server.start();
|
||||
|
||||
// BufferedResponseHandler.LOG.setDebugEnabled(true);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void tearDown() throws Exception
|
||||
{
|
||||
_server.stop();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void before()
|
||||
{
|
||||
_test._bufferSize=-1;
|
||||
_test._mimeType=null;
|
||||
_test._content=new byte[128];
|
||||
Arrays.fill(_test._content,(byte)'X');
|
||||
_test._content[_test._content.length-1]='\n';
|
||||
_test._writes=10;
|
||||
_test._flush=false;
|
||||
_test._close=false;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNormal() throws Exception
|
||||
{
|
||||
String response = _local.getResponse("GET /ctx/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
assertThat(response,containsString(" 200 OK"));
|
||||
assertThat(response,containsString("Write: 0"));
|
||||
assertThat(response,containsString("Write: 7"));
|
||||
assertThat(response,not(containsString("Content-Length: ")));
|
||||
assertThat(response,not(containsString("Write: 8")));
|
||||
assertThat(response,not(containsString("Write: 9")));
|
||||
assertThat(response,not(containsString("Written: true")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIncluded() throws Exception
|
||||
{
|
||||
String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
assertThat(response,containsString(" 200 OK"));
|
||||
assertThat(response,containsString("Write: 0"));
|
||||
assertThat(response,containsString("Write: 9"));
|
||||
assertThat(response,containsString("Written: true"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExcludedByPath() throws Exception
|
||||
{
|
||||
String response = _local.getResponse("GET /ctx/include/path.exclude HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
assertThat(response,containsString(" 200 OK"));
|
||||
assertThat(response,containsString("Write: 0"));
|
||||
assertThat(response,containsString("Write: 7"));
|
||||
assertThat(response,not(containsString("Content-Length: ")));
|
||||
assertThat(response,not(containsString("Write: 8")));
|
||||
assertThat(response,not(containsString("Write: 9")));
|
||||
assertThat(response,not(containsString("Written: true")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExcludedByMime() throws Exception
|
||||
{
|
||||
_test._mimeType="text/excluded";
|
||||
String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
assertThat(response,containsString(" 200 OK"));
|
||||
assertThat(response,containsString("Write: 0"));
|
||||
assertThat(response,containsString("Write: 7"));
|
||||
assertThat(response,not(containsString("Content-Length: ")));
|
||||
assertThat(response,not(containsString("Write: 8")));
|
||||
assertThat(response,not(containsString("Write: 9")));
|
||||
assertThat(response,not(containsString("Written: true")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFlushed() throws Exception
|
||||
{
|
||||
_test._flush=true;
|
||||
String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
assertThat(response,containsString(" 200 OK"));
|
||||
assertThat(response,containsString("Write: 0"));
|
||||
assertThat(response,containsString("Write: 9"));
|
||||
assertThat(response,containsString("Written: true"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClosed() throws Exception
|
||||
{
|
||||
_test._close=true;
|
||||
String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
assertThat(response,containsString(" 200 OK"));
|
||||
assertThat(response,containsString("Write: 0"));
|
||||
assertThat(response,containsString("Write: 9"));
|
||||
assertThat(response,not(containsString("Written: true")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBufferSizeSmall() throws Exception
|
||||
{
|
||||
_test._bufferSize=16;
|
||||
String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
assertThat(response,containsString(" 200 OK"));
|
||||
assertThat(response,containsString("Write: 0"));
|
||||
assertThat(response,containsString("Write: 9"));
|
||||
assertThat(response,containsString("Written: true"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBufferSizeBig() throws Exception
|
||||
{
|
||||
_test._bufferSize=4096;
|
||||
String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
assertThat(response,containsString(" 200 OK"));
|
||||
assertThat(response,containsString("Content-Length: "));
|
||||
assertThat(response,containsString("Write: 0"));
|
||||
assertThat(response,containsString("Write: 9"));
|
||||
assertThat(response,containsString("Written: true"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOne() throws Exception
|
||||
{
|
||||
_test._writes=1;
|
||||
String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
assertThat(response,containsString(" 200 OK"));
|
||||
assertThat(response,containsString("Content-Length: "));
|
||||
assertThat(response,containsString("Write: 0"));
|
||||
assertThat(response,not(containsString("Write: 1")));
|
||||
assertThat(response,containsString("Written: true"));
|
||||
}
|
||||
|
||||
public static class TestHandler extends AbstractHandler
|
||||
{
|
||||
int _bufferSize;
|
||||
String _mimeType;
|
||||
byte[] _content;
|
||||
int _writes;
|
||||
boolean _flush;
|
||||
boolean _close;
|
||||
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||
{
|
||||
baseRequest.setHandled(true);
|
||||
if (_bufferSize>0)
|
||||
response.setBufferSize(_bufferSize);
|
||||
if (_mimeType!=null)
|
||||
response.setContentType(_mimeType);
|
||||
|
||||
for (int i=0;i<_writes;i++)
|
||||
{
|
||||
response.addHeader("Write",Integer.toString(i));
|
||||
response.getOutputStream().write(_content);
|
||||
if (_flush)
|
||||
response.getOutputStream().flush();
|
||||
}
|
||||
|
||||
if (_close)
|
||||
response.getOutputStream().close();
|
||||
response.addHeader("Written","true");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue