352 lines
13 KiB
Java
352 lines
13 KiB
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.valves;
|
|
|
|
import java.io.IOException;
|
|
import java.io.Writer;
|
|
import java.util.Scanner;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
|
import javax.servlet.RequestDispatcher;
|
|
import javax.servlet.ServletException;
|
|
import javax.servlet.http.HttpServletResponse;
|
|
|
|
import org.apache.catalina.connector.Request;
|
|
import org.apache.catalina.connector.Response;
|
|
import org.apache.catalina.util.ServerInfo;
|
|
import org.apache.catalina.util.TomcatCSS;
|
|
import org.apache.coyote.ActionCode;
|
|
import org.apache.tomcat.util.ExceptionUtils;
|
|
import org.apache.tomcat.util.res.StringManager;
|
|
import org.apache.tomcat.util.security.Escape;
|
|
|
|
/**
|
|
* <p>Implementation of a Valve that outputs HTML error pages.</p>
|
|
*
|
|
* <p>This Valve should be attached at the Host level, although it will work
|
|
* if attached to a Context.</p>
|
|
*
|
|
* <p>HTML code from the Cocoon 2 project.</p>
|
|
*
|
|
* @author Remy Maucherat
|
|
* @author Craig R. McClanahan
|
|
* @author <a href="mailto:nicolaken@supereva.it">Nicola Ken Barozzi</a> Aisa
|
|
* @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a>
|
|
* @author Yoav Shapira
|
|
*/
|
|
public class ErrorReportValve extends ValveBase {
|
|
|
|
private boolean showReport = true;
|
|
|
|
private boolean showServerInfo = true;
|
|
|
|
//------------------------------------------------------ Constructor
|
|
public ErrorReportValve() {
|
|
super(true);
|
|
}
|
|
|
|
|
|
// --------------------------------------------------------- Public Methods
|
|
|
|
/**
|
|
* Invoke the next Valve in the sequence. When the invoke returns, check
|
|
* the response state. If the status code is greater than or equal to 400
|
|
* or an uncaught exception was thrown then the error handling will be
|
|
* triggered.
|
|
*
|
|
* @param request The servlet request to be processed
|
|
* @param response The servlet response to be created
|
|
*
|
|
* @exception IOException if an input/output error occurs
|
|
* @exception ServletException if a servlet error occurs
|
|
*/
|
|
@Override
|
|
public void invoke(Request request, Response response) throws IOException, ServletException {
|
|
|
|
// Perform the request
|
|
getNext().invoke(request, response);
|
|
|
|
if (response.isCommitted()) {
|
|
if (response.setErrorReported()) {
|
|
// Error wasn't previously reported but we can't write an error
|
|
// page because the response has already been committed. Attempt
|
|
// to flush any data that is still to be written to the client.
|
|
try {
|
|
response.flushBuffer();
|
|
} catch (Throwable t) {
|
|
ExceptionUtils.handleThrowable(t);
|
|
}
|
|
// Close immediately to signal to the client that something went
|
|
// wrong
|
|
response.getCoyoteResponse().action(ActionCode.CLOSE_NOW,
|
|
request.getAttribute(RequestDispatcher.ERROR_EXCEPTION));
|
|
}
|
|
return;
|
|
}
|
|
|
|
Throwable throwable = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
|
|
|
|
// If an async request is in progress and is not going to end once this
|
|
// container thread finishes, do not process any error page here.
|
|
if (request.isAsync() && !request.isAsyncCompleting()) {
|
|
return;
|
|
}
|
|
|
|
if (throwable != null && !response.isError()) {
|
|
// Make sure that the necessary methods have been called on the
|
|
// response. (It is possible a component may just have set the
|
|
// Throwable. Tomcat won't do that but other components might.)
|
|
// These are safe to call at this point as we know that the response
|
|
// has not been committed.
|
|
response.reset();
|
|
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
|
}
|
|
|
|
// One way or another, response.sendError() will have been called before
|
|
// execution reaches this point and suspended the response. Need to
|
|
// reverse that so this valve can write to the response.
|
|
response.setSuspended(false);
|
|
|
|
try {
|
|
report(request, response, throwable);
|
|
} catch (Throwable tt) {
|
|
ExceptionUtils.handleThrowable(tt);
|
|
}
|
|
}
|
|
|
|
|
|
// ------------------------------------------------------ Protected Methods
|
|
|
|
|
|
/**
|
|
* Prints out an error report.
|
|
*
|
|
* @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 report(Request request, Response response, Throwable throwable) {
|
|
|
|
int statusCode = response.getStatus();
|
|
|
|
// Do nothing on a 1xx, 2xx and 3xx status
|
|
// Do nothing if anything has been written already
|
|
// Do nothing if the response hasn't been explicitly marked as in error
|
|
// and that error has not been reported.
|
|
if (statusCode < 400 || response.getContentWritten() > 0 || !response.setErrorReported()) {
|
|
return;
|
|
}
|
|
|
|
// 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()) {
|
|
return;
|
|
}
|
|
|
|
String message = Escape.htmlElementContent(response.getMessage());
|
|
if (message == null) {
|
|
if (throwable != null) {
|
|
String exceptionMessage = throwable.getMessage();
|
|
if (exceptionMessage != null && exceptionMessage.length() > 0) {
|
|
message = Escape.htmlElementContent((new Scanner(exceptionMessage)).nextLine());
|
|
}
|
|
}
|
|
if (message == null) {
|
|
message = "";
|
|
}
|
|
}
|
|
|
|
// Do nothing if there is no reason phrase for the specified status code and
|
|
// no error message provided
|
|
String reason = null;
|
|
String description = null;
|
|
StringManager smClient = StringManager.getManager(
|
|
Constants.Package, request.getLocales());
|
|
response.setLocale(smClient.getLocale());
|
|
try {
|
|
reason = smClient.getString("http." + statusCode + ".reason");
|
|
description = smClient.getString("http." + statusCode + ".desc");
|
|
} catch (Throwable t) {
|
|
ExceptionUtils.handleThrowable(t);
|
|
}
|
|
if (reason == null || description == null) {
|
|
if (message.isEmpty()) {
|
|
return;
|
|
} else {
|
|
reason = smClient.getString("errorReportValve.unknownReason");
|
|
description = smClient.getString("errorReportValve.noDescription");
|
|
}
|
|
}
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
sb.append("<!doctype html><html lang=\"");
|
|
sb.append(smClient.getLocale().getLanguage()).append("\">");
|
|
sb.append("<head>");
|
|
sb.append("<title>");
|
|
sb.append(smClient.getString("errorReportValve.statusHeader",
|
|
String.valueOf(statusCode), reason));
|
|
sb.append("</title>");
|
|
sb.append("<style type=\"text/css\">");
|
|
sb.append(TomcatCSS.TOMCAT_CSS);
|
|
sb.append("</style>");
|
|
sb.append("</head><body>");
|
|
sb.append("<h1>");
|
|
sb.append(smClient.getString("errorReportValve.statusHeader",
|
|
String.valueOf(statusCode), reason)).append("</h1>");
|
|
if (isShowReport()) {
|
|
sb.append("<hr class=\"line\" />");
|
|
sb.append("<p><b>");
|
|
sb.append(smClient.getString("errorReportValve.type"));
|
|
sb.append("</b> ");
|
|
if (throwable != null) {
|
|
sb.append(smClient.getString("errorReportValve.exceptionReport"));
|
|
} else {
|
|
sb.append(smClient.getString("errorReportValve.statusReport"));
|
|
}
|
|
sb.append("</p>");
|
|
if (!message.isEmpty()) {
|
|
sb.append("<p><b>");
|
|
sb.append(smClient.getString("errorReportValve.message"));
|
|
sb.append("</b> ");
|
|
sb.append(message).append("</p>");
|
|
}
|
|
sb.append("<p><b>");
|
|
sb.append(smClient.getString("errorReportValve.description"));
|
|
sb.append("</b> ");
|
|
sb.append(description);
|
|
sb.append("</p>");
|
|
if (throwable != null) {
|
|
String stackTrace = getPartialServletStackTrace(throwable);
|
|
sb.append("<p><b>");
|
|
sb.append(smClient.getString("errorReportValve.exception"));
|
|
sb.append("</b></p><pre>");
|
|
sb.append(Escape.htmlElementContent(stackTrace));
|
|
sb.append("</pre>");
|
|
|
|
int loops = 0;
|
|
Throwable rootCause = throwable.getCause();
|
|
while (rootCause != null && (loops < 10)) {
|
|
stackTrace = getPartialServletStackTrace(rootCause);
|
|
sb.append("<p><b>");
|
|
sb.append(smClient.getString("errorReportValve.rootCause"));
|
|
sb.append("</b></p><pre>");
|
|
sb.append(Escape.htmlElementContent(stackTrace));
|
|
sb.append("</pre>");
|
|
// In case root cause is somehow heavily nested
|
|
rootCause = rootCause.getCause();
|
|
loops++;
|
|
}
|
|
|
|
sb.append("<p><b>");
|
|
sb.append(smClient.getString("errorReportValve.note"));
|
|
sb.append("</b> ");
|
|
sb.append(smClient.getString("errorReportValve.rootCauseInLogs"));
|
|
sb.append("</p>");
|
|
|
|
}
|
|
sb.append("<hr class=\"line\" />");
|
|
}
|
|
if (isShowServerInfo()) {
|
|
sb.append("<h3>").append(ServerInfo.getServerInfo()).append("</h3>");
|
|
}
|
|
sb.append("</body></html>");
|
|
|
|
try {
|
|
try {
|
|
response.setContentType("text/html");
|
|
response.setCharacterEncoding("utf-8");
|
|
} catch (Throwable t) {
|
|
ExceptionUtils.handleThrowable(t);
|
|
if (container.getLogger().isDebugEnabled()) {
|
|
container.getLogger().debug("status.setContentType", t);
|
|
}
|
|
}
|
|
Writer writer = response.getReporter();
|
|
if (writer != null) {
|
|
// If writer is null, it's an indication that the response has
|
|
// been hard committed already, which should never happen
|
|
writer.write(sb.toString());
|
|
response.finishResponse();
|
|
}
|
|
} catch (IOException e) {
|
|
// Ignore
|
|
} catch (IllegalStateException e) {
|
|
// Ignore
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Print out a partial servlet stack trace (truncating at the last
|
|
* occurrence of javax.servlet.).
|
|
* @param t The stack trace to process
|
|
* @return the stack trace relative to the application layer
|
|
*/
|
|
protected String getPartialServletStackTrace(Throwable t) {
|
|
StringBuilder trace = new StringBuilder();
|
|
trace.append(t.toString()).append(System.lineSeparator());
|
|
StackTraceElement[] elements = t.getStackTrace();
|
|
int pos = elements.length;
|
|
for (int i = elements.length - 1; i >= 0; i--) {
|
|
if ((elements[i].getClassName().startsWith
|
|
("org.apache.catalina.core.ApplicationFilterChain"))
|
|
&& (elements[i].getMethodName().equals("internalDoFilter"))) {
|
|
pos = i;
|
|
break;
|
|
}
|
|
}
|
|
for (int i = 0; i < pos; i++) {
|
|
if (!(elements[i].getClassName().startsWith
|
|
("org.apache.catalina.core."))) {
|
|
trace.append('\t').append(elements[i].toString()).append(System.lineSeparator());
|
|
}
|
|
}
|
|
return trace.toString();
|
|
}
|
|
|
|
/**
|
|
* Enables/Disables full error reports
|
|
*
|
|
* @param showReport <code>true</code> to show full error data
|
|
*/
|
|
public void setShowReport(boolean showReport) {
|
|
this.showReport = showReport;
|
|
}
|
|
|
|
public boolean isShowReport() {
|
|
return showReport;
|
|
}
|
|
|
|
/**
|
|
* Enables/Disables server info on error pages
|
|
*
|
|
* @param showServerInfo <code>true</code> to show server info
|
|
*/
|
|
public void setShowServerInfo(boolean showServerInfo) {
|
|
this.showServerInfo = showServerInfo;
|
|
}
|
|
|
|
public boolean isShowServerInfo() {
|
|
return showServerInfo;
|
|
}
|
|
}
|