org.eclipse.jetty.server.internal.HttpChannelState Maven / Gradle / Ivy
Show all versions of jetty-server Show documentation
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server.internal;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.WritePendingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.ComplianceViolation;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.MultiPartFormData.Parts;
import org.eclipse.jetty.http.Trailers;
import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Components;
import org.eclipse.jetty.server.ConnectionMetaData;
import org.eclipse.jetty.server.Context;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpStream;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.RequestLog;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.server.TunnelSupport;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.ExceptionUtil;
import org.eclipse.jetty.util.NanoTime;
import org.eclipse.jetty.util.thread.AutoLock;
import org.eclipse.jetty.util.thread.Invocable;
import org.eclipse.jetty.util.thread.Scheduler;
import org.eclipse.jetty.util.thread.SerializedInvoker;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A Channel represents a sequence of request cycles from the same connection with only a single
* request cycle may be active at once for each channel.
*
* Many methods return {@link Runnable}s to indicate that further work is needed. These
* can be given to an ExecutionStrategy instead of calling known methods like HttpChannel.handle().
*/
public class HttpChannelState implements HttpChannel, Components
{
/**
* The state of the written response
*/
private enum StreamSendState
{
/** Last content not yet sent */
SENDING,
/** Last content sent, but send not yet completed */
LAST_SENDING,
/** Last content sent and completed */
LAST_COMPLETE
}
private static final Logger LOG = LoggerFactory.getLogger(HttpChannelState.class);
private static final Throwable NOTHING_TO_SEND = new Throwable("nothing_to_send");
private static final HttpField SERVER_VERSION = new ResponseHttpFields.PersistentPreEncodedHttpField(HttpHeader.SERVER, HttpConfiguration.SERVER_VERSION);
private static final HttpField POWERED_BY = new ResponseHttpFields.PersistentPreEncodedHttpField(HttpHeader.X_POWERED_BY, HttpConfiguration.SERVER_VERSION);
private final AutoLock _lock = new AutoLock();
private final HandlerInvoker _handlerInvoker = new HandlerInvoker();
private final ConnectionMetaData _connectionMetaData;
private final SerializedInvoker _readInvoker;
private final SerializedInvoker _writeInvoker;
private final ResponseHttpFields _responseHeaders = new ResponseHttpFields();
private Thread _handling;
private boolean _handled;
private StreamSendState _streamSendState = StreamSendState.SENDING;
private boolean _callbackCompleted = false;
private ChannelRequest _request;
private ChannelResponse _response;
private long _oldIdleTimeout;
private HttpStream _stream;
private long _committedContentLength = -1;
private Runnable _onContentAvailable;
private Predicate _onIdleTimeout;
private Content.Chunk _readFailure;
private Consumer _onFailure;
private Throwable _callbackFailure;
private Attributes _cache;
private boolean _expects100Continue;
private ComplianceViolation.Listener _complianceViolationListener;
public HttpChannelState(ConnectionMetaData connectionMetaData)
{
_connectionMetaData = connectionMetaData;
// The SerializedInvoker is used to prevent infinite recursion of callbacks calling methods calling callbacks etc.
_readInvoker = new HttpChannelSerializedInvoker();
_writeInvoker = new HttpChannelSerializedInvoker();
}
@Override
public void initialize()
{
List listeners = _connectionMetaData.getHttpConfiguration().getComplianceViolationListeners();
_complianceViolationListener = switch (listeners.size())
{
case 0 -> ComplianceViolation.Listener.NOOP;
case 1 -> listeners.get(0).initialize();
default -> new InitializedCompositeComplianceViolationListener(listeners);
};
}
@Override
public ComplianceViolation.Listener getComplianceViolationListener()
{
return _complianceViolationListener;
}
@Override
public void recycle()
{
try (AutoLock ignored = _lock.lock())
{
if (LOG.isDebugEnabled())
LOG.debug("recycling {}", this);
// Break the link between request and channel, so that
// applications cannot use request/response/callback anymore.
_request._httpChannelState = null;
// Recycle.
_responseHeaders.recycle();
_handling = null;
_handled = false;
_streamSendState = StreamSendState.SENDING;
_callbackCompleted = false;
_request = null;
_response = null;
_oldIdleTimeout = 0;
// Break the link between channel and stream.
_stream = null;
_committedContentLength = -1;
_onContentAvailable = null;
_onIdleTimeout = null;
_readFailure = null;
_onFailure = null;
_callbackFailure = null;
_expects100Continue = false;
_complianceViolationListener = null;
}
}
public HttpConfiguration getHttpConfiguration()
{
return _connectionMetaData.getHttpConfiguration();
}
public HttpStream getHttpStream()
{
try (AutoLock ignored = _lock.lock())
{
return _stream;
}
}
public void setHttpStream(HttpStream stream)
{
try (AutoLock ignored = _lock.lock())
{
_stream = stream;
}
}
public Server getServer()
{
return _connectionMetaData.getConnector().getServer();
}
@Override
public ConnectionMetaData getConnectionMetaData()
{
return _connectionMetaData;
}
@Override
public ByteBufferPool getByteBufferPool()
{
return getConnectionMetaData().getConnector().getByteBufferPool();
}
@Override
public Scheduler getScheduler()
{
return getServer().getScheduler();
}
@Override
public ThreadPool getThreadPool()
{
return getServer().getThreadPool();
}
@Override
public Attributes getCache()
{
if (_cache == null)
{
if (getConnectionMetaData().isPersistent())
_cache = new Attributes.Mapped(new HashMap<>());
else
_cache = Attributes.NULL;
}
return _cache;
}
/**
* Start request handling by returning a Runnable that will call {@link Handler#handle(Request, Response, Callback)}.
*
* @param request The request metadata to handle.
* @return A Runnable that will call {@link Handler#handle(Request, Response, Callback)}. Unlike all other {@link Runnable}s
* returned by HttpChannel methods, this runnable should not be mutually excluded or serialized. Specifically
* other {@link Runnable}s returned by methods on this class can be run concurrently with the {@link Runnable}
* returned from this method.
*/
public Runnable onRequest(MetaData.Request request)
{
if (LOG.isDebugEnabled())
LOG.debug("onRequest {} {}", request, this);
try (AutoLock ignored = _lock.lock())
{
if (_stream == null)
throw new IllegalStateException("No HttpStream");
if (_request != null)
throw new IllegalStateException("duplicate request");
_request = new ChannelRequest(this, request);
_response = new ChannelResponse(_request);
_expects100Continue = request.is100ContinueExpected();
HttpConfiguration httpConfiguration = getHttpConfiguration();
HttpFields.Mutable responseHeaders = _response.getHeaders();
if (httpConfiguration.getSendServerVersion())
responseHeaders.add(SERVER_VERSION);
if (httpConfiguration.getSendXPoweredBy())
responseHeaders.add(POWERED_BY);
if (httpConfiguration.getSendDateHeader())
responseHeaders.add(getConnectionMetaData().getConnector().getServer().getDateField());
long idleTO = httpConfiguration.getIdleTimeout();
_oldIdleTimeout = _stream.getIdleTimeout();
if (idleTO >= 0 && _oldIdleTimeout != idleTO)
_stream.setIdleTimeout(idleTO);
// This is deliberately not serialized to allow a handler to block.
return _handlerInvoker;
}
}
public Request getRequest()
{
try (AutoLock ignored = _lock.lock())
{
return _request;
}
}
public Response getResponse()
{
try (AutoLock ignored = _lock.lock())
{
return _response;
}
}
public boolean isRequestHandled()
{
try (AutoLock ignored = _lock.lock())
{
return _handling != null || _handled;
}
}
public Runnable onContentAvailable()
{
Runnable onContent;
try (AutoLock ignored = _lock.lock())
{
if (_request == null)
return null;
onContent = _onContentAvailable;
_onContentAvailable = null;
}
return _readInvoker.offer(onContent);
}
@Override
public Invocable.InvocationType getInvocationType()
{
// TODO Can this actually be done, as we may need to invoke other Runnables after onContent?
// Could we at least avoid the lock???
Runnable onContent;
try (AutoLock ignored = _lock.lock())
{
if (_request == null)
return null;
onContent = _onContentAvailable;
}
return Invocable.getInvocationType(onContent);
}
@Override
public Runnable onIdleTimeout(TimeoutException t)
{
try (AutoLock ignored = _lock.lock())
{
if (LOG.isDebugEnabled())
LOG.debug("onIdleTimeout {}", this, t);
Runnable invokeOnContentAvailable = null;
if (_readFailure == null)
{
// If there is demand, take the onContentAvailable runnable to invoke below.
invokeOnContentAvailable = _onContentAvailable;
_onContentAvailable = null;
// If there was demand, then arrange for the next read to return a transient chunk failure.
if (invokeOnContentAvailable != null)
_readFailure = Content.Chunk.from(t, false);
}
// If a write call is pending, take the writeCallback to fail below.
Runnable invokeWriteFailure = _response.lockedFailWrite(t, false);
// If there was a pending IO operation, deliver the idle timeout via them.
if (invokeOnContentAvailable != null || invokeWriteFailure != null)
return Invocable.combine(_readInvoker.offer(invokeOnContentAvailable), _writeInvoker.offer(invokeWriteFailure));
// Otherwise, if there are idle timeout listeners, ask them whether we should call onFailure.
Predicate onIdleTimeout = _onIdleTimeout;
if (onIdleTimeout != null)
{
return () ->
{
if (onIdleTimeout.test(t))
{
// If the idle timeout listener(s) return true, then we call onFailure and run any task it returns.
Runnable task = onFailure(t);
if (task != null)
task.run();
}
};
}
}
// Otherwise treat as a failure.
return onFailure(t);
}
@Override
public Runnable onFailure(Throwable x)
{
HttpStream stream;
Runnable task;
try (AutoLock ignored = _lock.lock())
{
if (LOG.isDebugEnabled())
LOG.debug("onFailure {}", this, x);
// If the channel doesn't have a stream, then the error is ignored.
stream = _stream;
if (stream == null)
return null;
if (_request == null)
{
// If the channel doesn't have a request, then the error must have occurred during the parsing of
// the request line / headers, so make a temp request for logging and producing an error response.
MetaData.Request errorRequest = new MetaData.Request("GET", HttpURI.from("/"), HttpVersion.HTTP_1_0, HttpFields.EMPTY);
_request = new ChannelRequest(this, errorRequest);
_response = new ChannelResponse(_request);
}
// If not handled, then we just fail the request callback
if (!_handled && _handling == null)
{
task = () -> _request._callback.failed(x);
}
else
{
// Set the failure to arrange for any subsequent reads or demands to fail.
if (_readFailure == null)
_readFailure = Content.Chunk.from(x, true);
else
ExceptionUtil.addSuppressedIfNotAssociated(_readFailure.getFailure(), x);
// If there is demand, take the onContentAvailable runnable to invoke below.
Runnable invokeOnContentAvailable = _onContentAvailable;
_onContentAvailable = null;
// If a write call is in progress, take the writeCallback to fail below.
Runnable invokeWriteFailure = _response.lockedFailWrite(x, true);
// Notify the failure listeners only once.
Consumer onFailure = _onFailure;
_onFailure = null;
Runnable invokeOnFailureListeners = onFailure == null ? null : () ->
{
try
{
if (LOG.isDebugEnabled())
LOG.debug("invokeListeners {} {}", HttpChannelState.this, onFailure, x);
onFailure.accept(x);
}
catch (Throwable throwable)
{
ExceptionUtil.addSuppressedIfNotAssociated(x, throwable);
}
};
// Serialize all the error actions.
task = Invocable.combine(_readInvoker.offer(invokeOnContentAvailable), _writeInvoker.offer(invokeWriteFailure), _readInvoker.offer(invokeOnFailureListeners));
}
}
// Consume content as soon as possible to open any flow control window.
Throwable unconsumed = stream.consumeAvailable();
if (unconsumed != null && LOG.isDebugEnabled())
LOG.debug("consuming content during error {}", unconsumed.toString());
return task;
}
public void addHttpStreamWrapper(Function onStreamEvent)
{
while (true)
{
HttpStream stream;
try (AutoLock ignored = _lock.lock())
{
stream = _stream;
}
if (_stream == null)
throw new IllegalStateException("No active stream");
HttpStream combined = onStreamEvent.apply(stream);
if (combined == null)
throw new IllegalArgumentException("Cannot remove stream");
if (combined == stream)
return;
try (AutoLock ignored = _lock.lock())
{
if (_stream != stream)
continue;
_stream = combined;
break;
}
}
}
private void resetResponse()
{
try (AutoLock ignored = _lock.lock())
{
if (_responseHeaders.isCommitted())
throw new IllegalStateException("response committed");
_responseHeaders.clear();
}
}
private Throwable lockedStreamSend(boolean last, long length)
{
assert _lock.isHeldByCurrentThread();
return switch (_streamSendState)
{
case SENDING ->
{
_streamSendState = last ? StreamSendState.LAST_SENDING : StreamSendState.SENDING;
yield null;
}
// There are many instances of code that wants to ensure the output is closed, so
// it does a redundant write(true, callback). Other code may do a write(false, callback) to ensure
// they are flushed. The DO_NOT_SEND option supports these by turning such writes into a NOOP.
case LAST_SENDING, LAST_COMPLETE -> (length > 0)
? new IllegalStateException("last already written")
: NOTHING_TO_SEND;
};
}
private void lockedStreamSendCompleted(boolean success)
{
assert _lock.isHeldByCurrentThread();
if (_streamSendState == StreamSendState.LAST_SENDING)
_streamSendState = success ? StreamSendState.LAST_COMPLETE : StreamSendState.SENDING;
}
private boolean lockedIsLastStreamSendCompleted()
{
assert _lock.isHeldByCurrentThread();
return _streamSendState == StreamSendState.LAST_COMPLETE;
}
private boolean lockedLastStreamSend()
{
assert _lock.isHeldByCurrentThread();
if (_streamSendState != StreamSendState.SENDING)
return false;
_streamSendState = StreamSendState.LAST_SENDING;
return true;
}
@Override
public String toString()
{
try (AutoLock lock = _lock.tryLock())
{
boolean held = lock.isHeldByCurrentThread();
return String.format("%s@%x{handling=%s, handled=%s, send=%s, completed=%s, request=%s}",
this.getClass().getSimpleName(),
hashCode(),
held ? _handling : "?",
held ? _handled : "?",
held ? _streamSendState : "?",
held ? _callbackCompleted : "?",
held ? _request : "?"
);
}
}
private class HandlerInvoker implements Invocable.Task, Callback
{
@Override
public void run()
{
// Once we switch to HANDLING state and beyond, then we assume that the
// application will call the callback, and thus any onFailure reports will not.
// However, if a thread calling the application throws, then that exception will be reported
// to the callback.
ChannelRequest request;
ChannelResponse response;
try (AutoLock ignored = _lock.lock())
{
assert _handling == null && !_handled;
_handling = Thread.currentThread();
request = _request;
response = _response;
}
if (LOG.isDebugEnabled())
LOG.debug("invoking handler in {}", HttpChannelState.this);
Server server = _connectionMetaData.getConnector().getServer();
try
{
String pathInContext = Request.getPathInContext(request);
if (pathInContext != null && !pathInContext.startsWith("/"))
{
String method = request.getMethod();
if (!HttpMethod.PRI.is(method) && !HttpMethod.CONNECT.is(method) && !HttpMethod.OPTIONS.is(method))
throw new BadMessageException("Bad URI path");
}
HttpURI uri = request.getHttpURI();
if (uri.hasViolations())
{
String badMessage = UriCompliance.checkUriCompliance(getConnectionMetaData().getHttpConfiguration().getUriCompliance(), uri, HttpChannel.from(request).getComplianceViolationListener());
if (badMessage != null)
throw new BadMessageException(badMessage);
}
// Customize before processing.
HttpConfiguration configuration = getHttpConfiguration();
Request customized = request;
HttpFields.Mutable responseHeaders = response.getHeaders();
for (HttpConfiguration.Customizer customizer : configuration.getCustomizers())
{
Request next = customizer.customize(customized, responseHeaders);
customized = next == null ? customized : next;
}
if (customized != request && server.getRequestLog() != null)
request.setLoggedRequest(customized);
if (!server.handle(customized, response, request._callback))
Response.writeError(customized, response, request._callback, HttpStatus.NOT_FOUND_404);
}
catch (Throwable t)
{
request._callback.failed(t);
}
HttpStream stream;
Throwable failure;
boolean completeStream;
boolean callbackCompleted;
boolean lastStreamSendComplete;
try (AutoLock ignored = _lock.lock())
{
stream = _stream;
_handling = null;
_handled = true;
failure = _callbackFailure;
callbackCompleted = _callbackCompleted;
lastStreamSendComplete = lockedIsLastStreamSendCompleted();
completeStream = callbackCompleted && lastStreamSendComplete;
if (LOG.isDebugEnabled())
LOG.debug("handler invoked: completeStream={} failure={} callbackCompleted={} {}", completeStream, failure, callbackCompleted, HttpChannelState.this);
}
if (LOG.isDebugEnabled())
LOG.debug("stream={}, failure={}, callbackCompleted={}, completeStream={}", stream, failure, callbackCompleted, completeStream);
if (completeStream)
{
if (LOG.isDebugEnabled())
LOG.debug("completeStream({}, {})", stream, Objects.toString(failure));
completeStream(stream, failure);
}
}
/**
* Called only as {@link Callback} by last write from {@link ChannelCallback#succeeded}
*/
@Override
public void succeeded()
{
HttpStream stream;
boolean completeStream;
try (AutoLock ignored = _lock.lock())
{
assert _callbackCompleted;
_streamSendState = StreamSendState.LAST_COMPLETE;
completeStream = _handling == null;
stream = _stream;
}
if (completeStream)
completeStream(stream, null);
}
/**
* Called only as {@link Callback} by last send from {@link ChannelCallback#succeeded}
*/
@Override
public void failed(Throwable failure)
{
HttpStream stream;
boolean completeStream;
try (AutoLock ignored = _lock.lock())
{
assert _callbackCompleted;
_streamSendState = StreamSendState.LAST_COMPLETE;
completeStream = _handling == null;
stream = _stream;
failure = _callbackFailure = ExceptionUtil.combine(_callbackFailure, failure);
}
if (completeStream)
completeStream(stream, failure);
}
private void completeStream(HttpStream stream, Throwable failure)
{
try
{
RequestLog requestLog = getServer().getRequestLog();
if (requestLog != null)
{
if (LOG.isDebugEnabled())
LOG.debug("logging {}", HttpChannelState.this);
requestLog.log(_request.getLoggedRequest(), _response);
}
// Clean up any multipart tmp files and release any associated resources.
Parts parts = (Parts)_request.getAttribute(Parts.class.getName());
if (parts != null)
parts.close();
long idleTO = getHttpConfiguration().getIdleTimeout();
if (idleTO > 0 && _oldIdleTimeout != idleTO)
stream.setIdleTimeout(_oldIdleTimeout);
}
finally
{
// This is THE ONLY PLACE the stream is succeeded or failed.
if (failure == null)
stream.succeeded();
else
stream.failed(failure);
}
}
@Override
public InvocationType getInvocationType()
{
return getConnectionMetaData().getConnector().getServer().getInvocationType();
}
}
public static class ChannelRequest extends Attributes.Lazy implements Request
{
private final long _headersNanoTime = NanoTime.now();
private final ChannelCallback _callback = new ChannelCallback(this);
private final String _id;
private final ConnectionMetaData _connectionMetaData;
private final MetaData.Request _metaData;
private final AutoLock _lock;
private final LongAdder _contentBytesRead = new LongAdder();
private HttpChannelState _httpChannelState;
private Request _loggedRequest;
private HttpFields _trailers;
ChannelRequest(HttpChannelState httpChannelState, MetaData.Request metaData)
{
_httpChannelState = Objects.requireNonNull(httpChannelState);
_id = httpChannelState.getHttpStream().getId(); // Copy ID now, as stream will ultimately be nulled
_connectionMetaData = httpChannelState.getConnectionMetaData();
_metaData = Objects.requireNonNull(metaData);
_lock = httpChannelState._lock;
}
public void setLoggedRequest(Request request)
{
_loggedRequest = request;
}
public Request getLoggedRequest()
{
return _loggedRequest == null ? this : _loggedRequest;
}
HttpStream getHttpStream()
{
return getHttpChannelState()._stream;
}
public long getContentBytesRead()
{
return _contentBytesRead.longValue();
}
@Override
public String getId()
{
return _id;
}
@Override
public Components getComponents()
{
return getHttpChannelState();
}
@Override
public ConnectionMetaData getConnectionMetaData()
{
return _connectionMetaData;
}
private HttpChannelState getHttpChannelState()
{
try (AutoLock ignore = _lock.lock())
{
return lockedGetHttpChannelState();
}
}
private HttpChannelState lockedGetHttpChannelState()
{
assert _lock.isHeldByCurrentThread();
if (_httpChannelState == null)
throw new IllegalStateException("channel already completed");
return _httpChannelState;
}
@Override
public String getMethod()
{
return _metaData.getMethod();
}
@Override
public HttpURI getHttpURI()
{
return _metaData.getHttpURI();
}
@Override
public Context getContext()
{
return getConnectionMetaData().getConnector().getServer().getContext();
}
@Override
public HttpFields getHeaders()
{
return _metaData.getHttpFields();
}
@Override
public HttpFields getTrailers()
{
return _trailers;
}
@Override
public long getBeginNanoTime()
{
return _metaData.getBeginNanoTime();
}
@Override
public long getHeadersNanoTime()
{
return _headersNanoTime;
}
@Override
public boolean isSecure()
{
return HttpScheme.HTTPS.is(getHttpURI().getScheme());
}
@Override
public long getLength()
{
return _metaData.getContentLength();
}
@Override
public Content.Chunk read()
{
try
{
HttpStream stream;
try (AutoLock ignored = _lock.lock())
{
HttpChannelState httpChannel = lockedGetHttpChannelState();
Content.Chunk error = httpChannel._readFailure;
httpChannel._readFailure = Content.Chunk.next(error);
if (error != null)
return error;
stream = httpChannel._stream;
}
Content.Chunk chunk = stream.read();
if (LOG.isDebugEnabled())
LOG.debug("read {}", chunk);
if (chunk != null && chunk.hasRemaining())
_contentBytesRead.add(chunk.getByteBuffer().remaining());
if (chunk instanceof Trailers trailers)
_trailers = trailers.getTrailers();
return chunk;
}
catch (Throwable t)
{
return Content.Chunk.from(t, true);
}
}
@Override
public boolean consumeAvailable()
{
HttpStream stream;
try (AutoLock ignored = _lock.lock())
{
HttpChannelState httpChannel = lockedGetHttpChannelState();
stream = httpChannel._stream;
}
return stream.consumeAvailable() == null;
}
@Override
public void demand(Runnable demandCallback)
{
boolean error;
HttpStream stream;
HttpChannelState httpChannelState;
InterimCallback interimCallback = null;
try (AutoLock ignored = _lock.lock())
{
httpChannelState = lockedGetHttpChannelState();
stream = httpChannelState._stream;
error = httpChannelState._readFailure != null;
if (LOG.isDebugEnabled())
LOG.debug("demand {}", httpChannelState);
if (!error)
{
if (httpChannelState._onContentAvailable != null)
throw new IllegalArgumentException("demand pending");
httpChannelState._onContentAvailable = demandCallback;
if (httpChannelState._expects100Continue && httpChannelState._response._writeCallback == null)
{
httpChannelState._response._writeCallback = interimCallback = new InterimCallback(httpChannelState);
httpChannelState._expects100Continue = false;
}
}
}
if (error)
{
httpChannelState._readInvoker.run(demandCallback);
}
else if (interimCallback == null)
{
stream.demand();
}
else
{
stream.send(_metaData, new MetaData.Response(HttpStatus.CONTINUE_100, null, getConnectionMetaData().getHttpVersion(), HttpFields.EMPTY), false, null, interimCallback);
interimCallback.whenComplete((v, t) -> stream.demand());
}
}
@Override
public void fail(Throwable failure)
{
Runnable runnable = _httpChannelState.onFailure(failure);
if (runnable != null)
getContext().execute(runnable);
}
@Override
public void push(MetaData.Request resource)
{
getHttpStream().push(resource);
}
@Override
public void addIdleTimeoutListener(Predicate onIdleTimeout)
{
try (AutoLock ignored = _lock.lock())
{
HttpChannelState httpChannel = lockedGetHttpChannelState();
if (httpChannel._readFailure != null)
return;
if (httpChannel._onIdleTimeout == null)
{
httpChannel._onIdleTimeout = onIdleTimeout;
}
else
{
Predicate previous = httpChannel._onIdleTimeout;
httpChannel._onIdleTimeout = throwable ->
{
if (!previous.test(throwable))
return onIdleTimeout.test(throwable);
return true;
};
}
}
}
@Override
public void addFailureListener(Consumer onFailure)
{
try (AutoLock ignored = _lock.lock())
{
HttpChannelState httpChannel = lockedGetHttpChannelState();
if (httpChannel._readFailure != null)
return;
if (httpChannel._onFailure == null)
{
httpChannel._onFailure = onFailure;
}
else
{
Consumer previous = httpChannel._onFailure;
httpChannel._onFailure = throwable ->
{
try
{
previous.accept(throwable);
}
catch (Throwable t)
{
ExceptionUtil.addSuppressedIfNotAssociated(throwable, t);
}
finally
{
onFailure.accept(throwable);
}
};
}
}
}
@Override
public TunnelSupport getTunnelSupport()
{
return getHttpStream().getTunnelSupport();
}
@Override
public void addHttpStreamWrapper(Function wrapper)
{
getHttpChannelState().addHttpStreamWrapper(wrapper);
}
@Override
public Session getSession(boolean create)
{
return null;
}
@Override
public String toString()
{
return String.format("%s@%x %s %s", getMethod(), hashCode(), getHttpURI(), _metaData.getHttpVersion());
}
}
/**
* The Channel's implementation of the {@link Response} API.
* Also is a {@link Callback} used by the {@link #write(boolean, ByteBuffer, Callback)}
* method when calling
* {@link HttpStream#send(MetaData.Request, MetaData.Response, boolean, ByteBuffer, Callback)}
*/
public static class ChannelResponse implements Response, Callback
{
private static final CompletableFuture UNEXPECTED_100_CONTINUE = CompletableFuture.failedFuture(new IllegalStateException("100 not expected"));
private static final CompletableFuture COMMITTED_100_CONTINUE = CompletableFuture.failedFuture(new IllegalStateException("Committed"));
private final ChannelRequest _request;
private final ResponseHttpFields _httpFields;
protected int _status;
private long _contentBytesWritten;
private Supplier _trailers;
private Callback _writeCallback;
private Throwable _writeFailure;
private ChannelResponse(ChannelRequest request)
{
_request = request;
_httpFields = getResponseHttpFields(_request.lockedGetHttpChannelState());
}
protected ResponseHttpFields getResponseHttpFields(HttpChannelState httpChannelState)
{
return httpChannelState._responseHeaders;
}
protected ResponseHttpFields getResponseHttpFields()
{
return _httpFields;
}
private boolean lockedIsWriting()
{
assert _request._lock.isHeldByCurrentThread();
return _writeCallback != null;
}
private Runnable lockedFailWrite(Throwable x, boolean fatal)
{
assert _request._lock.isHeldByCurrentThread();
Callback writeCallback = _writeCallback;
_writeCallback = null;
if (writeCallback != null || fatal)
{
if (_writeFailure == null)
_writeFailure = x;
else
ExceptionUtil.addSuppressedIfNotAssociated(_writeFailure, x);
}
if (writeCallback == null)
return null;
return () -> HttpChannelState.failed(writeCallback, x);
}
public long getContentBytesWritten()
{
return _contentBytesWritten;
}
@Override
public Request getRequest()
{
return _request;
}
@Override
public int getStatus()
{
return _status;
}
@Override
public void setStatus(int code)
{
if (!isCommitted())
_status = code;
}
@Override
public HttpFields.Mutable getHeaders()
{
return _httpFields;
}
@Override
public Supplier getTrailersSupplier()
{
try (AutoLock ignored = _request._lock.lock())
{
return _trailers;
}
}
@Override
public void setTrailersSupplier(Supplier trailers)
{
try (AutoLock ignored = _request._lock.lock())
{
_trailers = trailers;
}
}
@Override
public void write(boolean last, ByteBuffer content, Callback callback)
{
long length = BufferUtil.length(content);
HttpChannelState httpChannelState;
HttpStream stream;
Throwable writeFailure;
MetaData.Response responseMetaData = null;
try (AutoLock ignored = _request._lock.lock())
{
httpChannelState = _request.lockedGetHttpChannelState();
long totalWritten = _contentBytesWritten + length;
writeFailure = _writeFailure;
if (writeFailure == null)
{
if (_writeCallback != null)
{
if (_writeCallback instanceof InterimCallback interimCallback)
{
// Do this write after the interim callback.
interimCallback.whenComplete((v, t) -> write(last, content, callback));
return;
}
writeFailure = new WritePendingException();
}
else
{
long committedContentLength = httpChannelState._committedContentLength;
long contentLength = committedContentLength >= 0 ? committedContentLength : getHeaders().getLongField(HttpHeader.CONTENT_LENGTH);
if (contentLength >= 0 && totalWritten != contentLength)
{
// If the content length were not compatible with what was written, then we need to abort.
String lengthError = null;
if (totalWritten > contentLength)
lengthError = "written %d > %d content-length";
else if (last && !(totalWritten == 0 && HttpMethod.HEAD.is(_request.getMethod())))
lengthError = "written %d < %d content-length";
if (lengthError != null)
{
String message = lengthError.formatted(totalWritten, contentLength);
if (LOG.isDebugEnabled())
LOG.debug("fail {} {}", callback, message);
writeFailure = new IOException(message);
}
}
}
}
// If no failure by this point, we can try to switch to sending state.
if (writeFailure == null)
writeFailure = httpChannelState.lockedStreamSend(last, length);
if (writeFailure == NOTHING_TO_SEND)
{
httpChannelState._writeInvoker.run(callback::succeeded);
return;
}
// Have we failed in some way?
if (writeFailure != null)
{
Throwable failure = writeFailure;
httpChannelState._writeInvoker.run(() -> HttpChannelState.failed(callback, failure));
return;
}
// No failure, do the actual stream send using the ChannelResponse as the callback.
_writeCallback = callback;
_contentBytesWritten = totalWritten;
stream = httpChannelState._stream;
if (_httpFields.commit())
responseMetaData = lockedPrepareResponse(httpChannelState, last);
}
if (LOG.isDebugEnabled())
LOG.debug("writing last={} {} {}", last, BufferUtil.toDetailString(content), this);
stream.send(_request._metaData, responseMetaData, last, content, this);
}
/**
* Called when the call to
* {@link HttpStream#send(MetaData.Request, MetaData.Response, boolean, ByteBuffer, Callback)}
* made by {@link ChannelResponse#write(boolean, ByteBuffer, Callback)} succeeds.
* The implementation maintains the {@link #_streamSendState} before taking
* and serializing the call to the {@link #_writeCallback}, which was set by the call to {@code write}.
*/
@Override
public void succeeded()
{
if (LOG.isDebugEnabled())
LOG.debug("write succeeded {}", this);
Callback callback;
HttpChannelState httpChannel;
try (AutoLock ignored = _request._lock.lock())
{
callback = _writeCallback;
_writeCallback = null;
httpChannel = _request.lockedGetHttpChannelState();
httpChannel.lockedStreamSendCompleted(true);
}
if (callback != null)
httpChannel._writeInvoker.run(callback::succeeded);
}
/**
* Called when the call to
* {@link HttpStream#send(MetaData.Request, MetaData.Response, boolean, ByteBuffer, Callback)}
* made by {@link ChannelResponse#write(boolean, ByteBuffer, Callback)} fails.
*
* The implementation maintains the {@link #_streamSendState} before taking
* and serializing the call to the {@link #_writeCallback}, which was set by the call to {@code write}.
*
* @param x The reason for the failure.
*/
@Override
public void failed(Throwable x)
{
if (LOG.isDebugEnabled())
LOG.debug("write failed {}", this, x);
Callback callback;
HttpChannelState httpChannel;
try (AutoLock ignored = _request._lock.lock())
{
_writeFailure = x;
callback = _writeCallback;
_writeCallback = null;
httpChannel = _request.lockedGetHttpChannelState();
httpChannel.lockedStreamSendCompleted(false);
}
if (callback != null)
httpChannel._writeInvoker.run(() -> HttpChannelState.failed(callback, x));
}
@Override
public InvocationType getInvocationType()
{
return Invocable.getInvocationType(_writeCallback);
}
@Override
public boolean isCommitted()
{
return _httpFields.isCommitted();
}
@Override
public boolean hasLastWrite()
{
try (AutoLock ignored = _request._lock.lock())
{
if (_request._httpChannelState == null)
return true;
return _request._httpChannelState._streamSendState != StreamSendState.SENDING;
}
}
@Override
public boolean isCompletedSuccessfully()
{
try (AutoLock ignored = _request._lock.lock())
{
if (_request._httpChannelState == null)
return false;
return _request._httpChannelState._callbackCompleted && _request._httpChannelState._callbackFailure == null;
}
}
@Override
public void reset()
{
_status = 0;
_trailers = null;
_contentBytesWritten = 0;
_request.getHttpChannelState().resetResponse();
}
@Override
public CompletableFuture writeInterim(int status, HttpFields headers)
{
if (!HttpStatus.isInterim(status))
return CompletableFuture.failedFuture(new IllegalArgumentException("Invalid interim status code: " + status));
HttpStream stream;
MetaData.Response response;
InterimCallback interimCallback;
try (AutoLock ignored = _request._lock.lock())
{
HttpChannelState httpChannelState = _request.lockedGetHttpChannelState();
stream = httpChannelState._stream;
if (status == HttpStatus.CONTINUE_100)
{
if (!httpChannelState._expects100Continue)
return UNEXPECTED_100_CONTINUE;
httpChannelState._expects100Continue = false;
}
if (_httpFields.isCommitted())
return status == HttpStatus.CONTINUE_100 ? COMMITTED_100_CONTINUE : CompletableFuture.failedFuture(new IllegalStateException("Committed"));
if (_writeCallback != null)
return CompletableFuture.failedFuture(new WritePendingException());
_writeCallback = interimCallback = new InterimCallback(httpChannelState);
HttpVersion version = httpChannelState.getConnectionMetaData().getHttpVersion();
response = new MetaData.Response(status, null, version, headers);
}
stream.send(_request._metaData, response, false, null, interimCallback);
return interimCallback;
}
MetaData.Response lockedPrepareResponse(HttpChannelState httpChannel, boolean last)
{
assert _request._lock.isHeldByCurrentThread();
// Assume 200 unless told otherwise.
if (_status == 0)
_status = HttpStatus.OK_200;
// Can we set the content length?
HttpFields.Mutable mutableHeaders = _httpFields.getMutableHttpFields();
httpChannel._committedContentLength = mutableHeaders.getLongField(HttpHeader.CONTENT_LENGTH);
if (last && httpChannel._committedContentLength < 0L)
{
httpChannel._committedContentLength = _contentBytesWritten;
mutableHeaders.put(HttpHeader.CONTENT_LENGTH, httpChannel._committedContentLength);
}
httpChannel._stream.prepareResponse(mutableHeaders);
return new MetaData.Response(
_status, null, httpChannel.getConnectionMetaData().getHttpVersion(),
_httpFields,
httpChannel._committedContentLength,
getTrailersSupplier()
);
}
@Override
public String toString()
{
return "%s@%x{%s,%s}".formatted(this.getClass().getSimpleName(), hashCode(), getStatus(), getRequest());
}
}
private static class ChannelCallback implements Callback
{
private final ChannelRequest _request;
private Throwable _completedBy;
private ChannelCallback(ChannelRequest request)
{
_request = request;
}
/**
* Called when the {@link Handler} (or it's delegates) succeeds the request handling.
*/
@Override
public void succeeded()
{
// Called when the request/response cycle is completing successfully.
HttpStream stream;
boolean needLastStreamSend;
HttpChannelState httpChannelState;
Throwable failure = null;
ChannelRequest request;
ChannelResponse response;
MetaData.Response responseMetaData = null;
boolean completeStream;
ErrorResponse errorResponse = null;
try (AutoLock ignored = _request._lock.lock())
{
request = _request;
httpChannelState = _request._httpChannelState;
response = httpChannelState._response;
stream = httpChannelState._stream;
// We are being tough on handler implementations and expect them
// to not have pending operations when calling succeeded or failed.
if (httpChannelState._onContentAvailable != null)
throw new IllegalStateException("demand pending");
if (response.lockedIsWriting())
throw new IllegalStateException("write pending");
if (lockedCompleteCallback())
return;
assert httpChannelState._callbackFailure == null;
needLastStreamSend = httpChannelState.lockedLastStreamSend();
completeStream = !needLastStreamSend && httpChannelState._handling == null && httpChannelState.lockedIsLastStreamSendCompleted();
if (needLastStreamSend)
response._writeCallback = httpChannelState._handlerInvoker;
if (httpChannelState._responseHeaders.commit())
responseMetaData = response.lockedPrepareResponse(httpChannelState, true);
long totalWritten = response._contentBytesWritten;
long committedContentLength = httpChannelState._committedContentLength;
if (committedContentLength >= 0 && committedContentLength != totalWritten && !(totalWritten == 0 && HttpMethod.HEAD.is(_request.getMethod())))
failure = new IOException("content-length %d != %d written".formatted(committedContentLength, totalWritten));
// Is the request fully consumed?
Throwable unconsumed = stream.consumeAvailable();
if (LOG.isDebugEnabled())
LOG.debug("consumeAvailable: {} {} ", unconsumed == null, httpChannelState);
if (unconsumed != null && httpChannelState.getConnectionMetaData().isPersistent())
failure = ExceptionUtil.combine(failure, unconsumed);
if (failure != null)
{
httpChannelState._callbackFailure = failure;
if (!stream.isCommitted())
errorResponse = new ErrorResponse(request);
else
completeStream = true;
}
}
if (LOG.isDebugEnabled())
LOG.debug("succeeded: failure={} needLastStreamSend={} {}", failure, needLastStreamSend, this);
if (errorResponse != null)
Response.writeError(request, errorResponse, new ErrorCallback(request, errorResponse, stream, failure), failure);
else if (needLastStreamSend)
stream.send(_request._metaData, responseMetaData, true, null, response);
else if (completeStream)
httpChannelState._handlerInvoker.completeStream(stream, failure);
else if (LOG.isDebugEnabled())
LOG.debug("No action on succeeded {}", this);
}
/**
* Called when the {@link Handler} (or it's delegates) fail the request handling.
*
* @param failure The reason for the failure.
*/
@Override
public void failed(Throwable failure)
{
try
{
// Called when the request/response cycle is completing with a failure.
HttpStream stream;
ChannelRequest request;
HttpChannelState httpChannelState;
ErrorResponse errorResponse = null;
try (AutoLock ignored = _request._lock.lock())
{
if (lockedCompleteCallback())
return;
httpChannelState = _request._httpChannelState;
stream = httpChannelState._stream;
request = _request;
assert httpChannelState._callbackFailure == null;
httpChannelState._callbackFailure = failure;
// Consume any input.
Throwable unconsumed = stream.consumeAvailable();
ExceptionUtil.addSuppressedIfNotAssociated(failure, unconsumed);
ChannelResponse response = httpChannelState._response;
if (LOG.isDebugEnabled())
LOG.debug("failed stream.isCommitted={}, response.isCommitted={} {}", stream.isCommitted(), response.isCommitted(), this);
// There may have been an attempt to write an error response that failed.
// Do not try to write again an error response if already committed.
if (!stream.isCommitted())
errorResponse = new ErrorResponse(request);
}
if (errorResponse != null)
Response.writeError(request, errorResponse, new ErrorCallback(request, errorResponse, stream, failure), failure);
else
_request.getHttpChannelState()._handlerInvoker.failed(failure);
}
catch (Throwable t)
{
ExceptionUtil.addSuppressedIfNotAssociated(t, failure);
throw t;
}
}
private boolean lockedCompleteCallback()
{
assert _request._lock.isHeldByCurrentThread();
HttpChannelState httpChannelState = _request._httpChannelState;
if (httpChannelState == null)
{
if (LOG.isDebugEnabled())
LOG.debug("already recycled after completion {} by", _request, _completedBy);
return true;
}
if (httpChannelState._callbackCompleted)
{
if (LOG.isDebugEnabled())
{
LOG.debug("already completed {} by", _request, _completedBy);
LOG.debug("Second complete", new Throwable("second complete"));
}
return true;
}
if (LOG.isDebugEnabled())
_completedBy = new Throwable(Thread.currentThread().getName());
httpChannelState._callbackCompleted = true;
return false;
}
@Override
public InvocationType getInvocationType()
{
// TODO review this as it is probably not correct
return _request.getHttpStream().getInvocationType();
}
}
/**
* Used as the {@link Response} when writing the error response
* from {@link HttpChannelState.ChannelCallback#failed(Throwable)}.
*/
private static class ErrorResponse extends ChannelResponse
{
public ErrorResponse(ChannelRequest request)
{
super(request);
_status = HttpStatus.INTERNAL_SERVER_ERROR_500;
}
@Override
protected ResponseHttpFields getResponseHttpFields(HttpChannelState httpChannelState)
{
httpChannelState._committedContentLength = -1;
HttpFields original = super.getResponseHttpFields(httpChannelState);
ResponseHttpFields httpFields = new ResponseHttpFields();
for (HttpField field : original)
{
HttpHeader header = field.getHeader();
if (header == HttpHeader.SERVER || header == HttpHeader.DATE)
httpFields.add(field);
}
return httpFields;
}
@Override
MetaData.Response lockedPrepareResponse(HttpChannelState httpChannelState, boolean last)
{
assert httpChannelState._request._lock.isHeldByCurrentThread();
MetaData.Response httpFields = super.lockedPrepareResponse(httpChannelState, last);
httpChannelState._response._status = _status;
HttpFields.Mutable originalResponseFields = httpChannelState._responseHeaders.getMutableHttpFields();
originalResponseFields.clear();
originalResponseFields.add(getResponseHttpFields());
return httpFields;
}
}
/**
* Used as the {@link Response} and {@link Callback} when writing the error response
* from {@link HttpChannelState.ChannelCallback#failed(Throwable)}.
*/
private static class ErrorCallback implements Callback
{
private final ChannelRequest _request;
private final ErrorResponse _errorResponse;
private final HttpStream _stream;
private final Throwable _failure;
public ErrorCallback(ChannelRequest request, ErrorResponse response, HttpStream stream, Throwable failure)
{
_request = request;
_errorResponse = response;
_stream = stream;
_failure = failure;
}
/**
* Called when the error write in {@link HttpChannelState.ChannelCallback#failed(Throwable)} succeeds.
*/
@Override
public void succeeded()
{
if (LOG.isDebugEnabled())
LOG.debug("ErrorWrite succeeded: {}", this);
boolean needLastWrite;
MetaData.Response responseMetaData = null;
HttpChannelState httpChannelState;
Throwable failure;
try (AutoLock ignored = _request._lock.lock())
{
httpChannelState = _request.getHttpChannelState();
failure = _failure;
// Did the ErrorHandler do the last write?
needLastWrite = httpChannelState.lockedLastStreamSend();
if (needLastWrite && _errorResponse.getResponseHttpFields().commit())
responseMetaData = _errorResponse.lockedPrepareResponse(httpChannelState, true);
}
if (needLastWrite)
{
_stream.send(_request._metaData, responseMetaData, true, null,
Callback.from(() -> httpChannelState._handlerInvoker.failed(failure),
x ->
{
ExceptionUtil.addSuppressedIfNotAssociated(failure, x);
httpChannelState._handlerInvoker.failed(failure);
}));
}
else
{
HttpChannelState.failed(httpChannelState._handlerInvoker, failure);
}
}
/**
* Called when the error write in {@link HttpChannelState.ChannelCallback#failed(Throwable)} fails.
*
* @param x The reason for the failure.
*/
@Override
public void failed(Throwable x)
{
if (LOG.isDebugEnabled())
LOG.debug("ErrorWrite failed: {}", this, x);
Throwable failure;
HttpChannelState httpChannelState;
try (AutoLock ignored = _request._lock.lock())
{
failure = _failure;
httpChannelState = _request.lockedGetHttpChannelState();
httpChannelState._response._status = _errorResponse._status;
}
ExceptionUtil.addSuppressedIfNotAssociated(failure, x);
HttpChannelState.failed(httpChannelState._handlerInvoker, failure);
}
@Override
public String toString()
{
return "%s@%x".formatted(getClass().getSimpleName(), hashCode());
}
}
private static class InterimCallback extends Callback.Completable
{
private final HttpChannelState _httpChannelState;
private InterimCallback(HttpChannelState httpChannelState)
{
_httpChannelState = httpChannelState;
}
@Override
public void succeeded()
{
completing();
super.succeeded();
}
@Override
public void failed(Throwable x)
{
try
{
completing();
super.failed(x);
}
catch (Throwable t)
{
ExceptionUtil.addSuppressedIfNotAssociated(t, x);
throw t;
}
}
private void completing()
{
try (AutoLock ignore = _httpChannelState._lock.lock())
{
// Allow other writes to proceed
if (_httpChannelState._response._writeCallback == this)
_httpChannelState._response._writeCallback = null;
}
}
}
private class HttpChannelSerializedInvoker extends SerializedInvoker
{
@Override
protected void onError(Runnable task, Throwable failure)
{
ChannelRequest request;
boolean callbackCompleted;
try (AutoLock ignore = _lock.lock())
{
callbackCompleted = _callbackCompleted;
request = _request;
}
if (request == null || callbackCompleted)
{
// It is too late to handle error.
super.onError(task, failure);
return;
}
Runnable failureTask = onFailure(failure);
if (failureTask != null)
failureTask.run();
}
}
/**
* A Listener that represents multiple user {@link ComplianceViolation.Listener} instances
*/
private static class InitializedCompositeComplianceViolationListener implements ComplianceViolation.Listener
{
private static final Logger LOG = LoggerFactory.getLogger(InitializedCompositeComplianceViolationListener.class);
private final List _listeners;
/**
* Construct a new ComplianceViolations that will initialize the list of listeners and notify events to all.
*
* @param unInitializedListeners the user listeners to initialized and notify. Null or empty list is not allowed..
*/
public InitializedCompositeComplianceViolationListener(List unInitializedListeners)
{
List initialized = null;
for (ComplianceViolation.Listener listener : unInitializedListeners)
{
ComplianceViolation.Listener listening = listener.initialize();
if (listening != listener)
{
initialized = new ArrayList<>(unInitializedListeners.size());
for (ComplianceViolation.Listener l : unInitializedListeners)
{
if (l == listener)
break;
initialized.add(l);
}
}
if (initialized != null)
initialized.add(listening);
}
_listeners = initialized == null ? unInitializedListeners : initialized;
}
@Override
public void onRequestEnd(Attributes request)
{
for (ComplianceViolation.Listener listener : _listeners)
{
try
{
listener.onRequestEnd(request);
}
catch (Exception e)
{
LOG.warn("Unable to notify ComplianceViolation.Listener implementation at {} of onRequestEnd {}", listener, request, e);
}
}
}
@Override
public void onRequestBegin(Attributes request)
{
for (ComplianceViolation.Listener listener : _listeners)
{
try
{
listener.onRequestBegin(request);
}
catch (Exception e)
{
LOG.warn("Unable to notify ComplianceViolation.Listener implementation at {} of onRequestBegin {}", listener, request, e);
}
}
}
@Override
public ComplianceViolation.Listener initialize()
{
throw new IllegalStateException("already initialized");
}
@Override
public void onComplianceViolation(ComplianceViolation.Event event)
{
assert event != null;
for (ComplianceViolation.Listener listener : _listeners)
{
try
{
listener.onComplianceViolation(event);
}
catch (Exception e)
{
LOG.warn("Unable to notify ComplianceViolation.Listener implementation at {} of event {}", listener, event, e);
}
}
}
}
/**
* Invoke a callback failure, handling any {@link Throwable} thrown
* by adding the passed {@code failure} as a suppressed with
* {@link ExceptionUtil#addSuppressedIfNotAssociated(Throwable, Throwable)}.
* @param callback The callback to fail
* @param failure The failure
* @throws RuntimeException If thrown, will have the {@code failure} added as a suppressed.
*/
private static void failed(Callback callback, Throwable failure)
{
try
{
callback.failed(failure);
}
catch (Throwable t)
{
ExceptionUtil.addSuppressedIfNotAssociated(t, failure);
throw t;
}
}
}