StandardHostValve.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.catalina.core;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.catalina.Context;
import org.apache.catalina.Globals;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.ClientAbortException;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
import org.apache.coyote.ActionCode;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.descriptor.web.ErrorPage;
import org.apache.tomcat.util.res.StringManager;
/**
* Valve that implements the default basic behavior for the <code>StandardHost</code> container implementation.
* <p>
* <b>USAGE CONSTRAINT</b>: This implementation is likely to be useful only when processing HTTP requests.
*
* @author Craig R. McClanahan
* @author Remy Maucherat
*/
final class StandardHostValve extends ValveBase {
private static final Log log = LogFactory.getLog(StandardHostValve.class);
private static final StringManager sm = StringManager.getManager(StandardHostValve.class);
// Saves a call to getClassLoader() on very request. Under high load these
// calls took just long enough to appear as a hot spot (although a very
// minor one) in a profiler.
private static final ClassLoader MY_CLASSLOADER = StandardHostValve.class.getClassLoader();
// ------------------------------------------------------ Constructor
StandardHostValve() {
super(true);
}
// --------------------------------------------------------- Public Methods
/**
* Select the appropriate child Context to process this request, based on the specified request URI. If no matching
* Context can be found, return an appropriate HTTP error.
*
* @param request Request to be processed
* @param response Response to be produced
*
* @exception IOException if an input/output error occurred
* @exception ServletException if a servlet error occurred
*/
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
// Select the Context to be used for this Request
Context context = request.getContext();
if (context == null) {
// Don't overwrite an existing error
if (!response.isError()) {
response.sendError(404);
}
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(context.getPipeline().isAsyncSupported());
}
boolean asyncAtStart = request.isAsync();
try {
context.bind(MY_CLASSLOADER);
if (!asyncAtStart && !context.fireRequestInitEvent(request.getRequest())) {
// Don't fire listeners during async processing (the listener
// fired for the request that called startAsync()).
// If a request init listener throws an exception, the request
// is aborted.
return;
}
// Ask this Context to process this request. Requests that are
// already in error must have been routed here to check for
// application defined error pages so DO NOT forward them to the
// application for processing.
try {
if (!response.isErrorReportRequired()) {
context.getPipeline().getFirst().invoke(request, response);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
container.getLogger().error(sm.getString("standardHostValve.exception", request.getRequestURI()), t);
// If a new error occurred while trying to report a previous
// error allow the original error to be reported.
if (!response.isErrorReportRequired()) {
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
throwable(request, response, t);
}
}
// Now that the request/response pair is back under container
// control lift the suspension so that the error handling can
// complete and/or the container can flush any remaining data
response.setSuspended(false);
Throwable t = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
// Protect against NPEs if the context was destroyed during a
// long running request.
if (!context.getState().isAvailable()) {
return;
}
// Look for (and render if found) an application level error page
if (response.isErrorReportRequired()) {
// If an error has occurred that prevents further I/O, don't waste time
// producing an error report that will never be read
AtomicBoolean result = new AtomicBoolean(false);
response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result);
if (result.get()) {
if (t != null) {
throwable(request, response, t);
} else {
status(request, response);
}
}
}
if (!request.isAsync() && !asyncAtStart) {
context.fireRequestDestroyEvent(request.getRequest());
}
} finally {
// Access a session (if present) to update last accessed time, based
// on a strict interpretation of the specification
if (context.getAlwaysAccessSession()) {
request.getSession(false);
}
context.unbind(MY_CLASSLOADER);
}
}
// -------------------------------------------------------- Private Methods
/**
* Handle the HTTP status code (and corresponding message) generated while processing the specified Request to
* produce the specified Response. Any exceptions that occur during generation of the error report are logged and
* swallowed.
*
* @param request The request being processed
* @param response The response being generated
*/
private void status(Request request, Response response) {
int statusCode = response.getStatus();
// Handle a custom error page for this status code
Context context = request.getContext();
if (context == null) {
return;
}
/*
* Only look for error pages when isError() is set. isError() is set when response.sendError() is invoked. This
* allows custom error pages without relying on default from web.xml.
*/
if (!response.isError()) {
return;
}
ErrorPage errorPage = context.findErrorPage(statusCode);
if (errorPage == null) {
// Look for a default error page
errorPage = context.findErrorPage(0);
}
if (errorPage != null && response.isErrorReportRequired()) {
response.setAppCommitted(false);
setRequestErrorAttributes(request, statusCode, null, response.getMessage(), null, errorPage.getLocation());
if (custom(request, response, errorPage)) {
response.setErrorReported();
try {
response.finishResponse();
} catch (ClientAbortException e) {
// Ignore
} catch (IOException e) {
container.getLogger().warn(sm.getString("standardHostValve.exception", errorPage), e);
}
}
}
}
/**
* Handle the specified Throwable encountered while processing the specified Request to produce the specified
* Response. Any exceptions that occur during generation of the exception report are logged and swallowed.
*
* @param request The request being processed
* @param response The response being generated
* @param throwable The exception that occurred (which possibly wraps a root cause exception
*/
protected void throwable(Request request, Response response, Throwable throwable) {
Context context = request.getContext();
if (context == null) {
return;
}
Throwable realError = throwable;
if (realError instanceof ServletException) {
realError = ((ServletException) realError).getRootCause();
if (realError == null) {
realError = throwable;
}
}
// If this is an aborted request from a client just log it and return
if (realError instanceof ClientAbortException) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("standardHost.clientAbort", realError.getCause().getMessage()));
}
return;
}
ErrorPage errorPage = context.findErrorPage(throwable);
if ((errorPage == null) && (realError != throwable)) {
errorPage = context.findErrorPage(realError);
}
if (errorPage != null) {
if (response.setErrorReported()) {
response.setAppCommitted(false);
setRequestErrorAttributes(request, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, realError.getClass(),
throwable.getMessage(), realError, errorPage.getLocation());
if (custom(request, response, errorPage)) {
try {
response.finishResponse();
} catch (IOException e) {
container.getLogger().warn(sm.getString("standardHostValve.exception", errorPage), e);
}
}
}
} else {
/*
* A custom error-page has not been defined for the exception that was thrown during request processing. Set
* the status to 500 if an error status has not already been set and check for custom error-page for the
* status.
*/
if (response.getStatus() < HttpServletResponse.SC_BAD_REQUEST) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
// The response is an error
response.setError();
status(request, response);
}
}
private void setRequestErrorAttributes(Request request, int statusCode, Class<?> exceptionType, String message,
Throwable exception, String location) {
/*
* Generally, don't set attributes to null as that will trigger an unnecessary (in this case) call to
* removeAttribute().
*/
request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, Integer.valueOf(statusCode));
if (exceptionType != null) {
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION_TYPE, exceptionType);
}
/*
* https://bz.apache.org/bugzilla/show_bug.cgi?id=69444
*
* Need to ensure message attribute is set even if there is no message (e.g. if error was triggered by an
* exception with a null message).
*/
if (message == null) {
request.setAttribute(RequestDispatcher.ERROR_MESSAGE, "");
} else {
request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message);
}
if (exception != null) {
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, exception);
}
request.setAttribute(RequestDispatcher.ERROR_METHOD, request.getMethod());
request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI, request.getRequestURI());
request.setAttribute(RequestDispatcher.ERROR_QUERY_STRING, request.getQueryString());
Wrapper wrapper = request.getWrapper();
if (wrapper != null) {
request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME, wrapper.getName());
}
request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR, location);
request.setAttribute(Globals.DISPATCHER_TYPE_ATTR, DispatcherType.ERROR);
}
/**
* Handle an HTTP status code or Java exception by forwarding control to the location included in the specified
* errorPage object. It is assumed that the caller has already recorded any request attributes that are to be
* forwarded to this page. Return <code>true</code> if we successfully utilized the specified error page location,
* or <code>false</code> if the default error report should be rendered.
*
* @param request The request being processed
* @param response The response being generated
* @param errorPage The errorPage directive we are obeying
*/
private boolean custom(Request request, Response response, ErrorPage errorPage) {
if (container.getLogger().isTraceEnabled()) {
container.getLogger().trace("Processing " + errorPage);
}
try {
// Forward control to the specified location
ServletContext servletContext = request.getContext().getServletContext();
RequestDispatcher rd = servletContext.getRequestDispatcher(errorPage.getLocation());
if (rd == null) {
container.getLogger()
.error(sm.getString("standardHostValve.customStatusFailed", errorPage.getLocation()));
return false;
}
if (response.isCommitted()) {
// Response is committed - including the error page is the
// best we can do
rd.include(request.getRequest(), response.getResponse());
// Ensure the combined incomplete response and error page is
// written to the client
try {
response.flushBuffer();
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
}
// Now close immediately as an additional signal to the client
// that something went wrong
response.getCoyoteResponse().action(ActionCode.CLOSE_NOW,
request.getAttribute(RequestDispatcher.ERROR_EXCEPTION));
} else {
// Reset the response (keeping the real error code and message)
response.resetBuffer(true);
response.setContentLength(-1);
rd.forward(request.getRequest(), response.getResponse());
// If we forward, the response is suspended again
response.setSuspended(false);
}
// Indicate that we have successfully processed this custom page
return true;
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
// Report our failure to process this custom page
container.getLogger().error(sm.getString("standardHostValve.exception", errorPage), t);
return false;
}
}
}