Mercurial Hosting > luan
view src/org/eclipse/jetty/http/HttpGenerator.java @ 1072:00704b28b9f1
remove JBuffer.skip()
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Thu, 10 Nov 2016 02:29:01 -0700 |
parents | b4ba8a4d5a16 |
children | 6b7ff30bb990 |
line wrap: on
line source
// // ======================================================================== // Copyright (c) 1995-2014 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.http; import java.io.IOException; import java.io.InterruptedIOException; import java.util.Arrays; import org.eclipse.jetty.io.JBuffer; import org.eclipse.jetty.io.BufferUtil; import org.eclipse.jetty.io.Buffers; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.EofException; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.server.Server; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /* ------------------------------------------------------------ */ /** * HttpGenerator. Builds HTTP Messages. * * * */ public final class HttpGenerator { private static final Logger LOG = LoggerFactory.getLogger(HttpGenerator.class); // Build cache of response lines for status private static class Status { byte[] _schemeCode; byte[] _responseLine; } private static final Status[] __status = new Status[HttpStatus.MAX_CODE+1]; static { final int versionLength=HttpVersions.HTTP_1_1_BYTES.length; for (int i=0;i<__status.length;i++) { HttpStatus.Code code = HttpStatus.getCode(i); if (code==null) continue; String reason=code.getMessage(); byte[] bytes=new byte[versionLength+5+reason.length()+2]; // HttpVersions.HTTP_1_1_BUFFER.peek(0,bytes, 0, versionLength); System.arraycopy(HttpVersions.HTTP_1_1_BYTES,0,bytes,0,versionLength); bytes[versionLength+0]=' '; bytes[versionLength+1]=(byte)('0'+i/100); bytes[versionLength+2]=(byte)('0'+(i%100)/10); bytes[versionLength+3]=(byte)('0'+(i%10)); bytes[versionLength+4]=' '; for (int j=0;j<reason.length();j++) bytes[versionLength+5+j]=(byte)reason.charAt(j); bytes[versionLength+5+reason.length()]=HttpTokens.CARRIAGE_RETURN; bytes[versionLength+6+reason.length()]=HttpTokens.LINE_FEED; __status[i] = new Status(); __status[i]._schemeCode = Arrays.copyOf(bytes,versionLength+5); __status[i]._responseLine = bytes; } } // common _content private static final byte[] LAST_CHUNK = { (byte) '0', (byte) '\015', (byte) '\012', (byte) '\015', (byte) '\012'}; private static final byte[] CONTENT_LENGTH_0 = StringUtil.getBytes("Content-Length: 0\015\012"); private static final byte[] CONNECTION_KEEP_ALIVE = StringUtil.getBytes("Connection: keep-alive\015\012"); private static final byte[] CONNECTION_CLOSE = StringUtil.getBytes("Connection: close\015\012"); private static final byte[] CONNECTION_ = StringUtil.getBytes("Connection: "); private static final byte[] CRLF = StringUtil.getBytes("\015\012"); private static final byte[] TRANSFER_ENCODING_CHUNKED = StringUtil.getBytes("Transfer-Encoding: chunked\015\012"); private static byte[] SERVER = StringUtil.getBytes("Server: Jetty("+Server.version+")\015\012"); // other statics private static final int CHUNK_SPACE = 12; // data private boolean _bypass = false; // True if _content buffer can be written directly to endp and bypass the content buffer private boolean _needCRLF = false; private boolean _needEOC = false; private boolean _bufferChunked = false; public void shutdown() { if (_persistent!=null && !_persistent && !_endp.isOutputShutdown()) { try { _endp.shutdownOutput(); } catch(IOException e) { LOG.trace("",e); } } } /* ------------------------------------------------------------ */ /** * Add content. * * @param content * @param last * @throws IllegalArgumentException if <code>content</code> is {@link JBuffer#isImmutable immutable}. * @throws IllegalStateException If the request is not expecting any more content, * or if the buffers are full and cannot be flushed. * @throws IOException if there is a problem flushing the buffers. */ public void addContent(JBuffer content, boolean last) throws IOException { if (_noContent) throw new IllegalStateException("NO CONTENT"); if (_last || _state==STATE_END) { LOG.warn("Ignoring extra content {}",content); // content.clear(); return; } _last = last; // Handle any unfinished business? if (_content!=null && _content.hasRemaining() || _bufferChunked) { if (_endp.isOutputShutdown()) throw new EofException(); flushBuffer(); if (_content != null && _content.hasRemaining()) { if (_bufferChunked) { JBuffer nc = _buffers.getBuffer(_content.remaining()+CHUNK_SPACE+content.remaining()); nc.putQ(_content); nc.putQ(HttpTokens.CRLF); BufferUtil.putHexInt(nc, content.remaining()); nc.putQ(HttpTokens.CRLF); nc.putQ(content); nc.flip(); content = nc; } else { JBuffer nc = _buffers.getBuffer(_content.remaining()+content.remaining()); nc.putQ(_content); nc.putQ(content); nc.flip(); content = nc; } } } _content = content; _contentWritten += content.remaining(); // Handle the _content if (_head) { // content.clear(); _content = null; } else if ((_buffer.position()==0) && _content.hasRemaining() && (_last || isCommitted() && _content.remaining()>1024)) { _bypass = true; } else if (!_bufferChunked) { //System.out.println("qqqqqqqqqqqqqqqqqqq c"); // Copy _content to buffer; _buffer.putQ(_content); if (!_content.hasRemaining()) _content = null; } } /* ------------------------------------------------------------ */ /** Prepare buffer for unchecked writes. * Prepare the generator buffer to receive unchecked writes * @return the available space in the buffer. * @throws IOException */ public void prepareUncheckedAddContent() throws IOException { if (_noContent) // return -1; throw new RuntimeException("_noContent"); if (_last || _state==STATE_END) // return -1; throw new RuntimeException("_last"); // Handle any unfinished business? if (_content != null && _content.hasRemaining() || _bufferChunked) { flushBuffer(); if (_content != null && _content.hasRemaining() || _bufferChunked) throw new IllegalStateException("FULL"); } _contentWritten -= _buffer.position(); } public boolean isBufferFull() { // Should we flush the buffers? return isBufferFull2() || _bufferChunked || _bypass /* || (_contentLength == HttpTokens.CHUNKED_CONTENT && _buffer.remaining() < CHUNK_SPACE)*/; } public void send1xx(int code) throws IOException { if (_state != STATE_HEADER) return; if (code<100||code>199) throw new IllegalArgumentException("!1xx"); Status status = __status[code]; if (status==null) throw new IllegalArgumentException(code+"?"); _header.putQ(status._responseLine); _header.putQ(HttpTokens.CRLF); try { // nasty semi busy flush! _header.flip(); while(_header.remaining()>0) { int len = _endp.flush(_header); if (len<0) throw new EofException(); if (len==0) Thread.sleep(100); } } catch(InterruptedException e) { LOG.debug("",e); throw new InterruptedIOException(e.toString()); } } public void completeHeader(HttpFields fields, boolean allContentAdded) throws IOException { if (_state != STATE_HEADER) return; // handle a reset if (_status==0) throw new EofException(); if (_last && !allContentAdded) throw new IllegalStateException("last?"); _last = _last | allContentAdded; boolean has_server = false; try { // Responses if (_version == HttpVersions.HTTP_0_9_ORDINAL) { _persistent = false; _contentLength = HttpTokens.EOF_CONTENT; _state = STATE_CONTENT; return; } else { if (_persistent==null) _persistent = (_version > HttpVersions.HTTP_1_0_ORDINAL); // add response line Status status = _status<__status.length?__status[_status]:null; if (status==null) { _header.putQ(HttpVersions.HTTP_1_1_BYTES); _header.putQ((byte) ' '); _header.putQ((byte) ('0' + _status / 100)); _header.putQ((byte) ('0' + (_status % 100) / 10)); _header.putQ((byte) ('0' + (_status % 10))); _header.putQ((byte) ' '); if (_reason==null) { _header.putQ((byte) ('0' + _status / 100)); _header.putQ((byte) ('0' + (_status % 100) / 10)); _header.putQ((byte) ('0' + (_status % 10))); } else _header.putQ(_reason); _header.putQ(HttpTokens.CRLF); } else { if (_reason==null) _header.putQ(status._responseLine); else { _header.putQ(status._schemeCode); _header.putQ(_reason); _header.putQ(HttpTokens.CRLF); } } if (_status<200 && _status>=100 ) { _noContent = true; _content = null; _buffer.clear(); // end the header. if (_status!=101 ) { _header.putQ(HttpTokens.CRLF); _state = STATE_CONTENT; return; } } else if (_status==204 || _status==304) { _noContent = true; _content = null; _buffer.clear(); } } // key field values HttpFields.Field content_length = null; HttpFields.Field transfer_encoding = null; boolean keep_alive = false; boolean close=false; boolean content_type=false; StringBuilder connection = null; if (fields != null) { int s=fields.size(); for (int f=0;f<s;f++) { HttpFields.Field field = fields.getField(f); if (field==null) continue; switch (field.getNameOrdinal()) { case HttpHeaders.CONTENT_LENGTH_ORDINAL: content_length = field; _contentLength = field.getLongValue(); if (_contentLength < _contentWritten || _last && _contentLength != _contentWritten) content_length = null; // write the field to the header buffer field.putTo(_header); break; case HttpHeaders.CONTENT_TYPE_ORDINAL: // if (BufferUtil.isPrefix(MimeTypes.MULTIPART_BYTERANGES_BUFFER, field.getValueBuffer())) if (field.getValue().startsWith(MimeTypes.MULTIPART_BYTERANGES)) _contentLength = HttpTokens.SELF_DEFINING_CONTENT; // write the field to the header buffer content_type=true; field.putTo(_header); break; case HttpHeaders.TRANSFER_ENCODING_ORDINAL: if (_version == HttpVersions.HTTP_1_1_ORDINAL) transfer_encoding = field; // Do NOT add yet! break; case HttpHeaders.CONNECTION_ORDINAL: int connection_value = field.getValueOrdinal(); switch (connection_value) { case -1: { String[] values = field.getValue().split(","); for (int i=0;values!=null && i<values.length;i++) { int ord = HttpHeaderValues.CACHE.getOrdinal(values[i].trim()); if (ord != -1) { switch(ord) { case HttpHeaderValues.CLOSE_ORDINAL: close=true; _persistent = false; keep_alive=false; if (!_persistent && _contentLength == HttpTokens.UNKNOWN_CONTENT) _contentLength = HttpTokens.EOF_CONTENT; break; case HttpHeaderValues.KEEP_ALIVE_ORDINAL: if (_version == HttpVersions.HTTP_1_0_ORDINAL) { keep_alive = true; _persistent = true; } break; default: if (connection==null) connection=new StringBuilder(); else connection.append(','); connection.append(values[i]); } } else { if (connection==null) connection=new StringBuilder(); else connection.append(','); connection.append(values[i]); } } break; } case HttpHeaderValues.UPGRADE_ORDINAL: { // special case for websocket connection ordering field.putTo(_header); continue; } case HttpHeaderValues.CLOSE_ORDINAL: { close=true; _persistent=false; if (!_persistent && _contentLength == HttpTokens.UNKNOWN_CONTENT) _contentLength = HttpTokens.EOF_CONTENT; break; } case HttpHeaderValues.KEEP_ALIVE_ORDINAL: { if (_version == HttpVersions.HTTP_1_0_ORDINAL) { keep_alive = true; _persistent=true; } break; } default: { if (connection==null) connection=new StringBuilder(); else connection.append(','); connection.append(field.getValue()); } } // Do NOT add yet! break; case HttpHeaders.SERVER_ORDINAL: has_server=true; field.putTo(_header); break; default: // write the field to the header buffer field.putTo(_header); } } } // Calculate how to end _content and connection, _content length and transfer encoding // settings. // From RFC 2616 4.4: // 1. No body for 1xx, 204, 304 & HEAD response // 2. Force _content-length? // 3. If Transfer-Encoding!=identity && HTTP/1.1 && !HttpConnection==close then chunk // 4. Content-Length // 5. multipart/byteranges // 6. close switch ((int) _contentLength) { case HttpTokens.UNKNOWN_CONTENT: // It may be that we have no _content, or perhaps _content just has not been // written yet? // Response known not to have a body if (_contentWritten == 0 && (_status < 200 || _status == 204 || _status == 304)) _contentLength = HttpTokens.NO_CONTENT; else if (_last) { // we have seen all the _content there is _contentLength = _contentWritten; if (content_length == null && !_noContent) { // known length but not actually set. _header.putQ(HttpHeaders.CONTENT_LENGTH_BYTES); _header.putQ(HttpTokens.COLON); _header.putQ((byte) ' '); BufferUtil.putDecLong(_header, _contentLength); _header.putQ(HttpTokens.CRLF); } } else { // No idea, so we must assume that a body is coming _contentLength = (!_persistent || _version < HttpVersions.HTTP_1_1_ORDINAL ) ? HttpTokens.EOF_CONTENT : HttpTokens.CHUNKED_CONTENT; } break; case HttpTokens.NO_CONTENT: if (content_length == null && _status >= 200 && _status != 204 && _status != 304) _header.putQ(CONTENT_LENGTH_0); break; case HttpTokens.EOF_CONTENT: _persistent = false; break; case HttpTokens.CHUNKED_CONTENT: break; default: // TODO - maybe allow forced chunking by setting te ??? break; } // Add transfer_encoding if needed if (_contentLength == HttpTokens.CHUNKED_CONTENT) { // try to use user supplied encoding as it may have other values. if (transfer_encoding != null && HttpHeaderValues.CHUNKED_ORDINAL != transfer_encoding.getValueOrdinal()) { String c = transfer_encoding.getValue(); if (c.endsWith(HttpHeaderValues.CHUNKED)) transfer_encoding.putTo(_header); else throw new IllegalArgumentException("BAD TE"); } else _header.putQ(TRANSFER_ENCODING_CHUNKED); } // Handle connection if need be if (_contentLength==HttpTokens.EOF_CONTENT) { keep_alive=false; _persistent=false; } if (!_persistent && (close || _version > HttpVersions.HTTP_1_0_ORDINAL)) { _header.putQ(CONNECTION_CLOSE); if (connection!=null) { _header.position(_header.position()-2); _header.putQ((byte)','); _header.putQ(connection.toString().getBytes()); _header.putQ(CRLF); } } else if (keep_alive) { _header.putQ(CONNECTION_KEEP_ALIVE); if (connection!=null) { _header.position(_header.position()-2); _header.putQ((byte)','); _header.putQ(connection.toString().getBytes()); _header.putQ(CRLF); } } else if (connection!=null) { _header.putQ(CONNECTION_); _header.putQ(connection.toString().getBytes()); _header.putQ(CRLF); } if (!has_server && _status>199) _header.putQ(SERVER); // end the header. _header.putQ(HttpTokens.CRLF); _state = STATE_CONTENT; } catch(ArrayIndexOutOfBoundsException e) { throw new RuntimeException("Header>"+_header.remaining(),e); } } /* ------------------------------------------------------------ */ /** * Complete the message. * * @throws IOException */ public void complete() throws IOException { if (_state == STATE_END) return; complete2(); if (_state < STATE_FLUSHING) { _state = STATE_FLUSHING; if (_contentLength == HttpTokens.CHUNKED_CONTENT) _needEOC = true; } flushBuffer(); } public int flushBuffer() throws IOException { try { if (_state == STATE_HEADER) throw new IllegalStateException("State==HEADER"); prepareBuffers(); int total= 0; int len = -1; int to_flush = flushMask(); int last_flush; do { last_flush = to_flush; switch (to_flush) { //qqq case 7: throw new IllegalStateException(); // should never happen! case 6: _header.flip(); _buffer.flip(); len = _endp.flush(_header, _buffer, null); _header.compact(); _buffer.compact(); break; case 5: _header.flip(); len = _endp.flush(_header, _content, null); _header.compact(); break; case 4: _header.flip(); len = _endp.flush(_header); _header.compact(); break; case 3: _buffer.flip(); len = _endp.flush(_buffer, _content, null); _buffer.compact(); break; case 2: _buffer.flip(); len = _endp.flush(_buffer); _buffer.compact(); break; case 1: len = _endp.flush(_content); break; case 0: { len = 0; // Nothing more we can write now. _header.clear(); _bypass = false; _bufferChunked = false; _buffer.clear(); // ? if (_contentLength == HttpTokens.CHUNKED_CONTENT) { // Special case handling for small left over buffer from // an addContent that caused a buffer flush. if (_content != null && _content.remaining() < _buffer.remaining() && _state != STATE_FLUSHING) { _buffer.putQ(_content); _content = null; } } // Are we completely finished for now? if (!_needCRLF && !_needEOC && (_content==null || !_content.hasRemaining())) { if (_state == STATE_FLUSHING) _state = STATE_END; if (_state==STATE_END && _persistent != null && !_persistent && _status!=100) _endp.shutdownOutput(); } else // Try to prepare more to write. prepareBuffers(); } } if (len > 0) total+=len; to_flush = flushMask(); } // loop while progress is being made (OR we have prepared some buffers that might make progress) while (len>0 || (to_flush!=0 && last_flush==0)); return total; } catch (IOException e) { LOG.trace("",e); throw (e instanceof EofException) ? e : new EofException(e); } } private int flushMask() { return ((_header.position() > 0)?4:0) | ((_buffer.position() > 0)?2:0) | ((_bypass && _content != null && _content.hasRemaining())?1:0); } private void prepareBuffers() { // if we are not flushing an existing chunk if (!_bufferChunked) { // Refill buffer if possible if (!_bypass && _content != null && _content.hasRemaining() && _buffer.hasRemaining()) { _buffer.putQ(_content); if (!_content.hasRemaining()) _content = null; } // Chunk buffer if need be if (_contentLength == HttpTokens.CHUNKED_CONTENT) { if (_bypass && _buffer.position()==0 && _content!=null) { // this is a bypass write int size = _content.remaining(); _bufferChunked = true; // if we need CRLF add this to header if (_needCRLF) { if (_header.position() > 0) throw new IllegalStateException("EOC"); _header.putQ(HttpTokens.CRLF); _needCRLF = false; } // Add the chunk size to the header BufferUtil.putHexInt(_header, size); _header.putQ(HttpTokens.CRLF); // Need a CRLF after the content _needCRLF = true; } else { int size = _buffer.position(); if (size > 0) { // Prepare a chunk! _bufferChunked = true; if (_needCRLF) { if (_header.position() > 0) throw new IllegalStateException("EOC"); _header.putQ(HttpTokens.CRLF); _needCRLF = false; } BufferUtil.putHexInt(_header, size); _header.putQ(HttpTokens.CRLF); // Add end chunk trailer. if (_buffer.remaining() >= 2) _buffer.putQ(HttpTokens.CRLF); else _needCRLF = true; } } // If we need EOC and everything written if (_needEOC && (_content == null || !_content.hasRemaining())) { if (_needCRLF && _buffer.remaining() >= HttpTokens.CRLF.length) { _buffer.putQ(HttpTokens.CRLF); _needCRLF = false; } if (!_needCRLF && _needEOC && _buffer.remaining() >= LAST_CHUNK.length) { if (!_head) { _buffer.putQ(LAST_CHUNK); _bufferChunked = true; } _needEOC = false; } } } } if (_content != null && !_content.hasRemaining()) _content = null; } @Override public String toString() { JBuffer header = _header; JBuffer buffer = _buffer; JBuffer content = _content; return String.format("%s{s=%d,h=%d,b=%d,c=%d}", getClass().getSimpleName(), _state, header == null ? -1 : header.position(), buffer == null ? -1 : buffer.position(), content == null ? -1 : content.remaining()); } // AbstractGenerator public static final boolean LAST=true; public static final boolean MORE=false; // states private final static int STATE_HEADER = 0; private final static int STATE_CONTENT = 2; private final static int STATE_FLUSHING = 3; private final static int STATE_END = 4; // data private final Buffers _buffers; // source of buffers private final EndPoint _endp; private int _state = STATE_HEADER; private int _status = 0; private int _version = HttpVersions.HTTP_1_1_ORDINAL; private JBuffer _reason; private long _contentWritten = 0; private long _contentLength = HttpTokens.UNKNOWN_CONTENT; private boolean _last = false; private boolean _head = false; private boolean _noContent = false; private Boolean _persistent = null; private final JBuffer _header; // JBuffer for HTTP header (and maybe small _content) private final JBuffer _buffer; // JBuffer for copy of passed _content private JBuffer _content; // JBuffer passed to addContent public HttpGenerator(Buffers buffers, EndPoint io) { this._buffers = buffers; this._endp = io; _header = _buffers.getHeader(); _buffer = _buffers.getBuffer(); } public final boolean isOpen() { return _endp.isOpen(); } public final void resetBuffer() { if(_state>=STATE_FLUSHING) throw new IllegalStateException("Flushed"); _last = false; _persistent = null; _contentWritten = 0; _contentLength = HttpTokens.UNKNOWN_CONTENT; _content = null; _buffer.clear(); } /* ------------------------------------------------------------ */ /** * @return Returns the contentBufferSize. */ public final int getContentBufferSize() { return _buffer.capacity(); } public final JBuffer getUncheckedBuffer() { return _buffer; } public final boolean isComplete() { return _state == STATE_END; } public final boolean isIdle() { return _state == STATE_HEADER && _status==0; } public final boolean isCommitted() { return _state != STATE_HEADER; } public final void setContentLength(long value) { if (value<0) _contentLength = HttpTokens.UNKNOWN_CONTENT; else _contentLength = value; } public final void setHead(boolean head) { _head = head; } /* ------------------------------------------------------------ */ /** * @return <code>false</code> if the connection should be closed after a request has been read, * <code>true</code> if it should be used for additional requests. */ public final boolean isPersistent() { return _persistent!=null ? _persistent.booleanValue() : _version>HttpVersions.HTTP_1_0_ORDINAL; } public final void setPersistent(boolean persistent) { _persistent = persistent; } /* ------------------------------------------------------------ */ /** * @param version The version of the client the response is being sent to (NB. Not the version * in the response, which is the version of the server). */ public final void setVersion(int version) { if (_state != STATE_HEADER) throw new IllegalStateException("STATE!=START "+_state); _version = version; } /* ------------------------------------------------------------ */ /** * @param status The status code to send. * @param reason the status message to send. */ public final void setResponse(int status, String reason) { if (_state != STATE_HEADER) throw new IllegalStateException("STATE!=START"); _status = status; if (reason!=null) { int len = reason.length(); // TODO don't hard code if (len>1024) len=1024; _reason = BufferUtil.newBuffer(len); for (int i=0;i<len;i++) { char ch = reason.charAt(i); if (ch!='\r'&&ch!='\n') _reason.putQ((byte)ch); else _reason.putQ((byte)' '); } _reason.flip(); } } public final void completeUncheckedAddContent() { _contentWritten += _buffer.position(); if (_head) _buffer.clear(); } private boolean isBufferFull2() { return !_buffer.hasRemaining() || _content!=null && _content.remaining()>0; } public final boolean isWritten() { return _contentWritten>0; } public final boolean isAllContentWritten() { return _contentLength>=0 && _contentWritten>=_contentLength; } private void complete2() throws IOException { if (_state == STATE_HEADER) { throw new IllegalStateException("State==HEADER"); } if (_contentLength >= 0 && _contentLength != _contentWritten && !_head) { if (LOG.isDebugEnabled()) LOG.debug("ContentLength written=="+_contentWritten+" != contentLength=="+_contentLength); _persistent = false; } } public final void flush(long maxIdleTime) throws IOException { // block until everything is flushed long now = System.currentTimeMillis(); long end = now+maxIdleTime; JBuffer content = _content; JBuffer buffer = _buffer; if (content!=null && content.remaining()>0 || buffer.position()>0 || isBufferFull()) { flushBuffer(); while (now<end && (content!=null && content.remaining()>0 || buffer.position()>0) && _endp.isOpen()&& !_endp.isOutputShutdown()) { blockForOutput(end-now); now = System.currentTimeMillis(); } } } /* ------------------------------------------------------------ */ /** * Utility method to send an error response. If the builder is not committed, this call is * equivalent to a setResponse, addContent and complete call. * * @param code The error code * @param reason The error reason * @param content Contents of the error page * @param close True if the connection should be closed * @throws IOException if there is a problem flushing the response */ public final void sendError(int code, String reason, String content, boolean close) throws IOException { if (close) _persistent=false; if (isCommitted()) { LOG.debug("sendError on committed: {} {}",code,reason); } else { LOG.debug("sendError: {} {}",code,reason); setResponse(code, reason); if (content != null) { completeHeader(null, false); addContent(BufferUtil.wrap(content), LAST); } else if (code>=400) { completeHeader(null, false); addContent(BufferUtil.wrap("Error: "+(reason==null?(""+code):reason)), LAST); } else { completeHeader(null, true); } complete(); } } public final long getContentWritten() { return _contentWritten; } public final void blockForOutput(long maxIdleTime) throws IOException { if (_endp.isBlocking()) { try { flushBuffer(); } catch(IOException e) { _endp.close(); throw e; } } else { if (!_endp.blockWritable(maxIdleTime)) { _endp.close(); throw new EofException("timeout"); } flushBuffer(); } } }