blob: a4f150f5f0bb033e2f2f0400a9e33b15e4906ae8 [file] [log] [blame]
/*
* 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 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.RequestUtil;
import org.apache.catalina.util.ServerInfo;
import org.apache.coyote.ActionCode;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.res.StringManager;
/**
* <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, null);
}
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;
}
String message = RequestUtil.filter(response.getMessage());
if (message == null) {
if (throwable != null) {
String exceptionMessage = throwable.getMessage();
if (exceptionMessage != null && exceptionMessage.length() > 0) {
message = RequestUtil.filter((new Scanner(exceptionMessage)).nextLine());
}
}
if (message == null) {
message = "";
}
}
// Do nothing if there is no report for the specified status code and
// no error message provided
String report = null;
StringManager smClient = StringManager.getManager(
Constants.Package, request.getLocales());
response.setLocale(smClient.getLocale());
try {
report = smClient.getString("http." + statusCode);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
}
if (report == null) {
if (message.length() == 0) {
return;
} else {
report = smClient.getString("errorReportValve.noDescription");
}
}
StringBuilder sb = new StringBuilder();
sb.append("<!DOCTYPE html><html><head>");
if(showServerInfo || showReport){
sb.append("<title>");
if(showServerInfo) {
sb.append(ServerInfo.getServerInfo()).append(" - ");
}
sb.append(smClient.getString("errorReportValve.errorReport"));
sb.append("</title>");
sb.append("<style type=\"text/css\">");
sb.append(org.apache.catalina.util.TomcatCSS.TOMCAT_CSS);
sb.append("</style> ");
} else {
sb.append("<title>");
sb.append(smClient.getString("errorReportValve.errorReport"));
sb.append("</title>");
}
sb.append("</head><body>");
sb.append("<h1>");
sb.append(smClient.getString("errorReportValve.statusHeader",
String.valueOf(statusCode), message)).append("</h1>");
if (showReport) {
sb.append("<div class=\"line\"></div>");
sb.append("<p><b>type</b> ");
if (throwable != null) {
sb.append(smClient.getString("errorReportValve.exceptionReport"));
} else {
sb.append(smClient.getString("errorReportValve.statusReport"));
}
sb.append("</p>");
sb.append("<p><b>");
sb.append(smClient.getString("errorReportValve.message"));
sb.append("</b> <u>");
sb.append(message).append("</u></p>");
sb.append("<p><b>");
sb.append(smClient.getString("errorReportValve.description"));
sb.append("</b> <u>");
sb.append(report);
sb.append("</u></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(RequestUtil.filter(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(RequestUtil.filter(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> <u>");
sb.append(smClient.getString("errorReportValve.rootCauseInLogs",
showServerInfo?ServerInfo.getServerInfo():""));
sb.append("</u></p>");
}
sb.append("<hr class=\"line\">");
}
if (showServerInfo) {
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.).
*/
protected String getPartialServletStackTrace(Throwable t) {
StringBuilder trace = new StringBuilder();
trace.append(t.toString()).append('\n');
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('\n');
}
}
return trace.toString();
}
/**
* Enables/Disables full error reports
*
* @param showReport
*/
public void setShowReport(boolean showReport) {
this.showReport = showReport;
}
public boolean isShowReport() {
return showReport;
}
/**
* Enables/Disables server info on error pages
*
* @param showServerInfo
*/
public void setShowServerInfo(boolean showServerInfo) {
this.showServerInfo = showServerInfo;
}
public boolean isShowServerInfo() {
return showServerInfo;
}
}