Fixes #612 - Support HTTP Trailer.
Implemented support in the generic HttpChannel and Request classes. Linked HTTP/1.1 and HTTP/2 trailers to call HttpChannel, so that trailers are now available in Servlet APIs by casting to Jetty's Request class. The semantic is that trailers will only be available _after_ reading the request content.
This commit is contained in:
parent
4f7c53b9b1
commit
7e7459d825
|
@ -497,7 +497,8 @@ public class HttpParser
|
||||||
_cr=true;
|
_cr=true;
|
||||||
if (buffer.hasRemaining())
|
if (buffer.hasRemaining())
|
||||||
{
|
{
|
||||||
if(_maxHeaderBytes>0 && _state.ordinal()<State.END.ordinal())
|
// Don't count the CRs and LFs of the chunked encoding.
|
||||||
|
if (_maxHeaderBytes>0 && (_state == State.HEADER || _state == State.TRAILER))
|
||||||
_headerBytes++;
|
_headerBytes++;
|
||||||
return next(buffer);
|
return next(buffer);
|
||||||
}
|
}
|
||||||
|
@ -509,7 +510,6 @@ public class HttpParser
|
||||||
case LEGAL:
|
case LEGAL:
|
||||||
if (_cr)
|
if (_cr)
|
||||||
throw new BadMessageException("Bad EOL");
|
throw new BadMessageException("Bad EOL");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ch;
|
return ch;
|
||||||
|
@ -867,7 +867,6 @@ public class HttpParser
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException(_state.toString());
|
throw new IllegalStateException(_state.toString());
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -993,10 +992,8 @@ public class HttpParser
|
||||||
*/
|
*/
|
||||||
protected boolean parseFields(ByteBuffer buffer)
|
protected boolean parseFields(ByteBuffer buffer)
|
||||||
{
|
{
|
||||||
boolean handle=false;
|
|
||||||
|
|
||||||
// Process headers
|
// Process headers
|
||||||
while ((_state==State.HEADER || _state==State.TRAILER) && buffer.hasRemaining() && !handle)
|
while ((_state==State.HEADER || _state==State.TRAILER) && buffer.hasRemaining())
|
||||||
{
|
{
|
||||||
// process each character
|
// process each character
|
||||||
byte ch=next(buffer);
|
byte ch=next(buffer);
|
||||||
|
@ -1005,8 +1002,11 @@ public class HttpParser
|
||||||
|
|
||||||
if (_maxHeaderBytes>0 && ++_headerBytes>_maxHeaderBytes)
|
if (_maxHeaderBytes>0 && ++_headerBytes>_maxHeaderBytes)
|
||||||
{
|
{
|
||||||
LOG.warn("Header is too large >"+_maxHeaderBytes);
|
boolean header = _state == State.HEADER;
|
||||||
throw new BadMessageException(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431);
|
LOG.warn("{} is too large {}>{}", header ? "Header" : "Trailer", _headerBytes, _maxHeaderBytes);
|
||||||
|
throw new BadMessageException(header ?
|
||||||
|
HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431 :
|
||||||
|
HttpStatus.PAYLOAD_TOO_LARGE_413);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (_fieldState)
|
switch (_fieldState)
|
||||||
|
@ -1084,29 +1084,34 @@ public class HttpParser
|
||||||
switch (_endOfContent)
|
switch (_endOfContent)
|
||||||
{
|
{
|
||||||
case EOF_CONTENT:
|
case EOF_CONTENT:
|
||||||
|
{
|
||||||
setState(State.EOF_CONTENT);
|
setState(State.EOF_CONTENT);
|
||||||
handle=_handler.headerComplete()||handle;
|
boolean handle=_handler.headerComplete();
|
||||||
_headerComplete=true;
|
_headerComplete=true;
|
||||||
return handle;
|
return handle;
|
||||||
|
}
|
||||||
case CHUNKED_CONTENT:
|
case CHUNKED_CONTENT:
|
||||||
|
{
|
||||||
setState(State.CHUNKED_CONTENT);
|
setState(State.CHUNKED_CONTENT);
|
||||||
handle=_handler.headerComplete()||handle;
|
boolean handle=_handler.headerComplete();
|
||||||
_headerComplete=true;
|
_headerComplete=true;
|
||||||
return handle;
|
return handle;
|
||||||
|
}
|
||||||
case NO_CONTENT:
|
case NO_CONTENT:
|
||||||
|
{
|
||||||
setState(State.END);
|
setState(State.END);
|
||||||
handle=_handler.headerComplete()||handle;
|
boolean handle=_handler.headerComplete();
|
||||||
_headerComplete=true;
|
_headerComplete=true;
|
||||||
handle=_handler.messageComplete()||handle;
|
handle=_handler.messageComplete()||handle;
|
||||||
return handle;
|
return handle;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
|
{
|
||||||
setState(State.CONTENT);
|
setState(State.CONTENT);
|
||||||
handle=_handler.headerComplete()||handle;
|
boolean handle=_handler.headerComplete();
|
||||||
_headerComplete=true;
|
_headerComplete=true;
|
||||||
return handle;
|
return handle;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1315,7 +1320,7 @@ public class HttpParser
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return handle;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------------------- */
|
/* ------------------------------------------------------------------------------- */
|
||||||
|
@ -1386,22 +1391,7 @@ public class HttpParser
|
||||||
while (buffer.remaining()>0 && buffer.get(buffer.position())<=HttpTokens.SPACE)
|
while (buffer.remaining()>0 && buffer.get(buffer.position())<=HttpTokens.SPACE)
|
||||||
buffer.get();
|
buffer.get();
|
||||||
}
|
}
|
||||||
else if (_state==State.CLOSE)
|
else if (isClose() || isClosed())
|
||||||
{
|
|
||||||
// Seeking EOF
|
|
||||||
if (BufferUtil.hasContent(buffer))
|
|
||||||
{
|
|
||||||
// Just ignore data when closed
|
|
||||||
_headerBytes+=buffer.remaining();
|
|
||||||
BufferUtil.clear(buffer);
|
|
||||||
if (_maxHeaderBytes>0 && _headerBytes>_maxHeaderBytes)
|
|
||||||
{
|
|
||||||
// Don't want to waste time reading data of a closed request
|
|
||||||
throw new IllegalStateException("too much data seeking EOF");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (_state==State.CLOSED)
|
|
||||||
{
|
{
|
||||||
BufferUtil.clear(buffer);
|
BufferUtil.clear(buffer);
|
||||||
}
|
}
|
||||||
|
@ -1449,55 +1439,33 @@ public class HttpParser
|
||||||
if (DEBUG)
|
if (DEBUG)
|
||||||
LOG.debug("{} EOF in {}",this,_state);
|
LOG.debug("{} EOF in {}",this,_state);
|
||||||
setState(State.CLOSED);
|
setState(State.CLOSED);
|
||||||
_handler.badMessage(400,null);
|
_handler.badMessage(HttpStatus.BAD_REQUEST_400,null);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(BadMessageException e)
|
catch(BadMessageException x)
|
||||||
{
|
{
|
||||||
BufferUtil.clear(buffer);
|
BufferUtil.clear(buffer);
|
||||||
|
badMessage(x);
|
||||||
Throwable cause = e.getCause();
|
|
||||||
boolean stack = LOG.isDebugEnabled() ||
|
|
||||||
(!(cause instanceof NumberFormatException ) && (cause instanceof RuntimeException || cause instanceof Error));
|
|
||||||
|
|
||||||
if (stack)
|
|
||||||
LOG.warn("bad HTTP parsed: "+e._code+(e.getReason()!=null?" "+e.getReason():"")+" for "+_handler,e);
|
|
||||||
else
|
|
||||||
LOG.warn("bad HTTP parsed: "+e._code+(e.getReason()!=null?" "+e.getReason():"")+" for "+_handler);
|
|
||||||
setState(State.CLOSE);
|
|
||||||
_handler.badMessage(e.getCode(), e.getReason());
|
|
||||||
}
|
}
|
||||||
catch(NumberFormatException|IllegalStateException e)
|
catch(Throwable x)
|
||||||
{
|
{
|
||||||
BufferUtil.clear(buffer);
|
BufferUtil.clear(buffer);
|
||||||
LOG.warn("parse exception: {} in {} for {}",e.toString(),_state,_handler);
|
badMessage(new BadMessageException(HttpStatus.BAD_REQUEST_400, _requestHandler != null ? "Bad Request" : "Bad Response", x));
|
||||||
if (DEBUG)
|
|
||||||
LOG.debug(e);
|
|
||||||
badMessage();
|
|
||||||
|
|
||||||
}
|
|
||||||
catch(Exception|Error e)
|
|
||||||
{
|
|
||||||
BufferUtil.clear(buffer);
|
|
||||||
LOG.warn("parse exception: "+e.toString()+" for "+_handler,e);
|
|
||||||
badMessage();
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void badMessage()
|
protected void badMessage(BadMessageException x)
|
||||||
{
|
{
|
||||||
|
if (DEBUG)
|
||||||
|
LOG.debug("Parse exception: " + this + " for " + _handler, x);
|
||||||
|
setState(State.CLOSE);
|
||||||
if (_headerComplete)
|
if (_headerComplete)
|
||||||
{
|
|
||||||
_handler.earlyEOF();
|
_handler.earlyEOF();
|
||||||
}
|
else
|
||||||
else if (_state!=State.CLOSED)
|
_handler.badMessage(x._code, x._reason);
|
||||||
{
|
|
||||||
setState(State.CLOSE);
|
|
||||||
_handler.badMessage(400,_requestHandler!=null?"Bad Request":"Bad Response");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean parseContent(ByteBuffer buffer)
|
protected boolean parseContent(ByteBuffer buffer)
|
||||||
|
|
|
@ -18,9 +18,17 @@
|
||||||
|
|
||||||
package org.eclipse.jetty.http2.client;
|
package org.eclipse.jetty.http2.client;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.ServletInputStream;
|
||||||
|
import javax.servlet.http.HttpServlet;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import org.eclipse.jetty.http.HttpFields;
|
import org.eclipse.jetty.http.HttpFields;
|
||||||
import org.eclipse.jetty.http.HttpStatus;
|
import org.eclipse.jetty.http.HttpStatus;
|
||||||
import org.eclipse.jetty.http.HttpVersion;
|
import org.eclipse.jetty.http.HttpVersion;
|
||||||
|
@ -28,7 +36,9 @@ import org.eclipse.jetty.http.MetaData;
|
||||||
import org.eclipse.jetty.http2.api.Session;
|
import org.eclipse.jetty.http2.api.Session;
|
||||||
import org.eclipse.jetty.http2.api.Stream;
|
import org.eclipse.jetty.http2.api.Stream;
|
||||||
import org.eclipse.jetty.http2.api.server.ServerSessionListener;
|
import org.eclipse.jetty.http2.api.server.ServerSessionListener;
|
||||||
|
import org.eclipse.jetty.http2.frames.DataFrame;
|
||||||
import org.eclipse.jetty.http2.frames.HeadersFrame;
|
import org.eclipse.jetty.http2.frames.HeadersFrame;
|
||||||
|
import org.eclipse.jetty.server.Request;
|
||||||
import org.eclipse.jetty.util.Callback;
|
import org.eclipse.jetty.util.Callback;
|
||||||
import org.eclipse.jetty.util.FuturePromise;
|
import org.eclipse.jetty.util.FuturePromise;
|
||||||
import org.eclipse.jetty.util.Promise;
|
import org.eclipse.jetty.util.Promise;
|
||||||
|
@ -83,6 +93,77 @@ public class TrailersTest extends AbstractTest
|
||||||
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
|
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testServletRequestTrailers() throws Exception
|
||||||
|
{
|
||||||
|
CountDownLatch trailerLatch = new CountDownLatch(1);
|
||||||
|
start(new HttpServlet()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
|
||||||
|
{
|
||||||
|
Request jettyRequest = (Request)request;
|
||||||
|
// No trailers yet.
|
||||||
|
Assert.assertNull(jettyRequest.getTrailers());
|
||||||
|
|
||||||
|
trailerLatch.countDown();
|
||||||
|
|
||||||
|
// Read the content.
|
||||||
|
ServletInputStream input = jettyRequest.getInputStream();
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int read = input.read();
|
||||||
|
if (read < 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we have the trailers.
|
||||||
|
HttpFields trailers = jettyRequest.getTrailers();
|
||||||
|
Assert.assertNotNull(trailers);
|
||||||
|
Assert.assertNotNull(trailers.get("X-Trailer"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Session session = newClient(new Session.Listener.Adapter());
|
||||||
|
|
||||||
|
HttpFields requestFields = new HttpFields();
|
||||||
|
requestFields.put("X-Request", "true");
|
||||||
|
MetaData.Request request = newRequest("GET", requestFields);
|
||||||
|
HeadersFrame requestFrame = new HeadersFrame(request, null, false);
|
||||||
|
FuturePromise<Stream> streamPromise = new FuturePromise<>();
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
session.newStream(requestFrame, streamPromise, new Stream.Listener.Adapter()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void onHeaders(Stream stream, HeadersFrame frame)
|
||||||
|
{
|
||||||
|
MetaData.Response response = (MetaData.Response)frame.getMetaData();
|
||||||
|
Assert.assertEquals(HttpStatus.OK_200, response.getStatus());
|
||||||
|
if (frame.isEndStream())
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Stream stream = streamPromise.get(5, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
// Send some data.
|
||||||
|
Callback.Completable callback = new Callback.Completable();
|
||||||
|
stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(16), false), callback);
|
||||||
|
|
||||||
|
Assert.assertTrue(trailerLatch.await(5, TimeUnit.SECONDS));
|
||||||
|
|
||||||
|
// Send the trailers.
|
||||||
|
callback.thenRun(() ->
|
||||||
|
{
|
||||||
|
HttpFields trailerFields = new HttpFields();
|
||||||
|
trailerFields.put("X-Trailer", "true");
|
||||||
|
MetaData trailers = new MetaData(HttpVersion.HTTP_2, trailerFields);
|
||||||
|
HeadersFrame trailerFrame = new HeadersFrame(stream.getId(), trailers, null, true);
|
||||||
|
stream.headers(trailerFrame, Callback.NOOP);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTrailersSentByServer() throws Exception
|
public void testTrailersSentByServer() throws Exception
|
||||||
{
|
{
|
||||||
|
|
|
@ -167,6 +167,15 @@ public class HTTP2ServerConnection extends HTTP2Connection implements Connection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onTrailers(IStream stream, HeadersFrame frame)
|
||||||
|
{
|
||||||
|
if (LOG.isDebugEnabled())
|
||||||
|
LOG.debug("Processing trailers {} on {}", frame, stream);
|
||||||
|
HttpChannelOverHTTP2 channel = (HttpChannelOverHTTP2)stream.getAttribute(IStream.CHANNEL_ATTRIBUTE);
|
||||||
|
if (channel != null)
|
||||||
|
channel.onRequestTrailers(frame);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean onStreamTimeout(IStream stream, Throwable failure)
|
public boolean onStreamTimeout(IStream stream, Throwable failure)
|
||||||
{
|
{
|
||||||
HttpChannelOverHTTP2 channel = (HttpChannelOverHTTP2)stream.getAttribute(IStream.CHANNEL_ATTRIBUTE);
|
HttpChannelOverHTTP2 channel = (HttpChannelOverHTTP2)stream.getAttribute(IStream.CHANNEL_ATTRIBUTE);
|
||||||
|
|
|
@ -142,8 +142,10 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF
|
||||||
@Override
|
@Override
|
||||||
public void onHeaders(Stream stream, HeadersFrame frame)
|
public void onHeaders(Stream stream, HeadersFrame frame)
|
||||||
{
|
{
|
||||||
// Servers do not receive responses.
|
if (frame.isEndStream())
|
||||||
close(stream, "response_headers");
|
getConnection().onTrailers((IStream)stream, frame);
|
||||||
|
else
|
||||||
|
close(stream, "invalid_trailers");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -274,6 +274,20 @@ public class HttpChannelOverHTTP2 extends HttpChannel
|
||||||
return handle || wasDelayed ? this : null;
|
return handle || wasDelayed ? this : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onRequestTrailers(HeadersFrame frame)
|
||||||
|
{
|
||||||
|
HttpFields trailers = frame.getMetaData().getFields();
|
||||||
|
onTrailers(trailers);
|
||||||
|
onRequestComplete();
|
||||||
|
if (LOG.isDebugEnabled())
|
||||||
|
{
|
||||||
|
Stream stream = getStream();
|
||||||
|
LOG.debug("HTTP2 Request #{}/{}, trailers:{}{}",
|
||||||
|
stream.getId(), Integer.toHexString(stream.getSession().hashCode()),
|
||||||
|
System.lineSeparator(), trailers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isRequestHandled()
|
public boolean isRequestHandled()
|
||||||
{
|
{
|
||||||
return _handled;
|
return _handled;
|
||||||
|
|
|
@ -586,6 +586,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
|
||||||
return _request.getHttpInput().addContent(content);
|
return _request.getHttpInput().addContent(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onTrailers(HttpFields trailers)
|
||||||
|
{
|
||||||
|
_request.setTrailers(trailers);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean onRequestComplete()
|
public boolean onRequestComplete()
|
||||||
{
|
{
|
||||||
if (LOG.isDebugEnabled())
|
if (LOG.isDebugEnabled())
|
||||||
|
|
|
@ -62,6 +62,7 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
|
||||||
private boolean _expect100Continue = false;
|
private boolean _expect100Continue = false;
|
||||||
private boolean _expect102Processing = false;
|
private boolean _expect102Processing = false;
|
||||||
private List<String> _complianceViolations;
|
private List<String> _complianceViolations;
|
||||||
|
private HttpFields _trailers;
|
||||||
|
|
||||||
public HttpChannelOverHttp(HttpConnection httpConnection, Connector connector, HttpConfiguration config, EndPoint endPoint, HttpTransport transport)
|
public HttpChannelOverHttp(HttpConnection httpConnection, Connector connector, HttpConfiguration config, EndPoint endPoint, HttpTransport transport)
|
||||||
{
|
{
|
||||||
|
@ -87,6 +88,7 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
|
||||||
_connection = null;
|
_connection = null;
|
||||||
_fields.clear();
|
_fields.clear();
|
||||||
_upgrade = null;
|
_upgrade = null;
|
||||||
|
_trailers = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -187,6 +189,14 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
|
||||||
_fields.add(field);
|
_fields.add(field);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void parsedTrailer(HttpField field)
|
||||||
|
{
|
||||||
|
if (_trailers == null)
|
||||||
|
_trailers = new HttpFields();
|
||||||
|
_trailers.add(field);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the associated response has the Expect header set to 100 Continue,
|
* If the associated response has the Expect header set to 100 Continue,
|
||||||
* then accessing the input stream indicates that the handler/servlet
|
* then accessing the input stream indicates that the handler/servlet
|
||||||
|
@ -460,6 +470,8 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
|
||||||
@Override
|
@Override
|
||||||
public boolean messageComplete()
|
public boolean messageComplete()
|
||||||
{
|
{
|
||||||
|
if (_trailers != null)
|
||||||
|
onTrailers(_trailers);
|
||||||
boolean handle = onRequestComplete() || _delayedForContent;
|
boolean handle = onRequestComplete() || _delayedForContent;
|
||||||
_delayedForContent = false;
|
_delayedForContent = false;
|
||||||
return handle;
|
return handle;
|
||||||
|
|
|
@ -416,10 +416,13 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
|
||||||
|
|
||||||
// Reset the channel, parsers and generator
|
// Reset the channel, parsers and generator
|
||||||
_channel.recycle();
|
_channel.recycle();
|
||||||
if (_generator.isPersistent() && !_parser.isClosed())
|
if (!_parser.isClosed())
|
||||||
_parser.reset();
|
{
|
||||||
else
|
if (_generator.isPersistent())
|
||||||
_parser.close();
|
_parser.reset();
|
||||||
|
else
|
||||||
|
_parser.close();
|
||||||
|
}
|
||||||
|
|
||||||
// Not in a race here with onFillable, because it has given up control before calling handle.
|
// Not in a race here with onFillable, because it has given up control before calling handle.
|
||||||
// in a slight race with #completed, but not sure what to do with that anyway.
|
// in a slight race with #completed, but not sure what to do with that anyway.
|
||||||
|
|
|
@ -166,14 +166,11 @@ public class Request implements HttpServletRequest
|
||||||
private final HttpChannel _channel;
|
private final HttpChannel _channel;
|
||||||
private final List<ServletRequestAttributeListener> _requestAttributeListeners=new ArrayList<>();
|
private final List<ServletRequestAttributeListener> _requestAttributeListeners=new ArrayList<>();
|
||||||
private final HttpInput _input;
|
private final HttpInput _input;
|
||||||
|
|
||||||
private MetaData.Request _metaData;
|
private MetaData.Request _metaData;
|
||||||
private String _originalURI;
|
private String _originalURI;
|
||||||
|
|
||||||
private String _contextPath;
|
private String _contextPath;
|
||||||
private String _servletPath;
|
private String _servletPath;
|
||||||
private String _pathInfo;
|
private String _pathInfo;
|
||||||
|
|
||||||
private boolean _secure;
|
private boolean _secure;
|
||||||
private String _asyncNotSupportedSource = null;
|
private String _asyncNotSupportedSource = null;
|
||||||
private boolean _newContext;
|
private boolean _newContext;
|
||||||
|
@ -202,6 +199,7 @@ public class Request implements HttpServletRequest
|
||||||
private long _timeStamp;
|
private long _timeStamp;
|
||||||
private MultiPartInputStreamParser _multiPartInputStream; //if the request is a multi-part mime
|
private MultiPartInputStreamParser _multiPartInputStream; //if the request is a multi-part mime
|
||||||
private AsyncContextState _async;
|
private AsyncContextState _async;
|
||||||
|
private HttpFields _trailers;
|
||||||
|
|
||||||
/* ------------------------------------------------------------ */
|
/* ------------------------------------------------------------ */
|
||||||
public Request(HttpChannel channel, HttpInput input)
|
public Request(HttpChannel channel, HttpInput input)
|
||||||
|
@ -217,6 +215,11 @@ public class Request implements HttpServletRequest
|
||||||
return metadata==null?null:metadata.getFields();
|
return metadata==null?null:metadata.getFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public HttpFields getTrailers()
|
||||||
|
{
|
||||||
|
return _trailers;
|
||||||
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------ */
|
/* ------------------------------------------------------------ */
|
||||||
public HttpInput getHttpInput()
|
public HttpInput getHttpInput()
|
||||||
{
|
{
|
||||||
|
@ -1847,6 +1850,7 @@ public class Request implements HttpServletRequest
|
||||||
_multiPartInputStream = null;
|
_multiPartInputStream = null;
|
||||||
_remote=null;
|
_remote=null;
|
||||||
_input.recycle();
|
_input.recycle();
|
||||||
|
_trailers = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------ */
|
/* ------------------------------------------------------------ */
|
||||||
|
@ -2215,6 +2219,11 @@ public class Request implements HttpServletRequest
|
||||||
_scope = scope;
|
_scope = scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setTrailers(HttpFields trailers)
|
||||||
|
{
|
||||||
|
_trailers = trailers;
|
||||||
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------ */
|
/* ------------------------------------------------------------ */
|
||||||
@Override
|
@Override
|
||||||
public AsyncContext startAsync() throws IllegalStateException
|
public AsyncContext startAsync() throws IllegalStateException
|
||||||
|
|
|
@ -0,0 +1,163 @@
|
||||||
|
//
|
||||||
|
// ========================================================================
|
||||||
|
// Copyright (c) 1995-2017 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;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.ServletInputStream;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.http.HttpFields;
|
||||||
|
import org.eclipse.jetty.http.HttpStatus;
|
||||||
|
import org.eclipse.jetty.http.HttpTester;
|
||||||
|
import org.eclipse.jetty.server.handler.AbstractHandler;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class HttpTrailersTest
|
||||||
|
{
|
||||||
|
private Server server;
|
||||||
|
private ServerConnector connector;
|
||||||
|
|
||||||
|
private void start(Handler handler) throws Exception
|
||||||
|
{
|
||||||
|
server = new Server();
|
||||||
|
connector = new ServerConnector(server);
|
||||||
|
server.addConnector(connector);
|
||||||
|
server.setHandler(handler);
|
||||||
|
server.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void dispose() throws Exception
|
||||||
|
{
|
||||||
|
if (server != null)
|
||||||
|
server.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testServletRequestTrailers() throws Exception
|
||||||
|
{
|
||||||
|
String trailerName = "Trailer";
|
||||||
|
String trailerValue = "value";
|
||||||
|
start(new AbstractHandler.ErrorDispatchHandler()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected void doNonErrorHandle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||||
|
{
|
||||||
|
jettyRequest.setHandled(true);
|
||||||
|
|
||||||
|
// Read the content first.
|
||||||
|
ServletInputStream input = jettyRequest.getInputStream();
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int read = input.read();
|
||||||
|
if (read < 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now the trailers can be accessed.
|
||||||
|
HttpFields trailers = jettyRequest.getTrailers();
|
||||||
|
Assert.assertNotNull(trailers);
|
||||||
|
Assert.assertEquals(trailerValue, trailers.get(trailerName));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try (Socket client = new Socket("localhost", connector.getLocalPort()))
|
||||||
|
{
|
||||||
|
client.setSoTimeout(5000);
|
||||||
|
|
||||||
|
String request = "" +
|
||||||
|
"GET / HTTP/1.1\r\n" +
|
||||||
|
"Host: localhost\r\n" +
|
||||||
|
"Transfer-Encoding: chunked\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
"0\r\n" +
|
||||||
|
trailerName + ": " + trailerValue + "\r\n" +
|
||||||
|
"\r\n";
|
||||||
|
OutputStream output = client.getOutputStream();
|
||||||
|
output.write(request.getBytes(StandardCharsets.UTF_8));
|
||||||
|
output.flush();
|
||||||
|
|
||||||
|
HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(client.getInputStream()));
|
||||||
|
Assert.assertNotNull(response);
|
||||||
|
Assert.assertEquals(HttpStatus.OK_200, response.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHugeTrailer() throws Exception
|
||||||
|
{
|
||||||
|
start(new AbstractHandler.ErrorDispatchHandler()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected void doNonErrorHandle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||||
|
{
|
||||||
|
jettyRequest.setHandled(true);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// EOF will not be reached because of the huge trailer.
|
||||||
|
ServletInputStream input = jettyRequest.getInputStream();
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int read = input.read();
|
||||||
|
if (read < 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Assert.fail();
|
||||||
|
}
|
||||||
|
catch (IOException x)
|
||||||
|
{
|
||||||
|
// Expected.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
char[] huge = new char[1024 * 1024];
|
||||||
|
Arrays.fill(huge, 'X');
|
||||||
|
try (Socket client = new Socket("localhost", connector.getLocalPort()))
|
||||||
|
{
|
||||||
|
client.setSoTimeout(5000);
|
||||||
|
|
||||||
|
String request = "" +
|
||||||
|
"GET / HTTP/1.1\r\n" +
|
||||||
|
"Host: localhost\r\n" +
|
||||||
|
"Transfer-Encoding: chunked\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
"0\r\n" +
|
||||||
|
"Trailer: " + new String(huge) + "\r\n" +
|
||||||
|
"\r\n";
|
||||||
|
OutputStream output = client.getOutputStream();
|
||||||
|
output.write(request.getBytes(StandardCharsets.UTF_8));
|
||||||
|
output.flush();
|
||||||
|
|
||||||
|
HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(client.getInputStream()));
|
||||||
|
Assert.assertNotNull(response);
|
||||||
|
Assert.assertEquals(HttpStatus.OK_200, response.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue