blob: a4e949821ba9f3b941cd4b94c52d4b3a6e4f3a03 [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.sling.servlets.resolver.internal.defaults;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import javax.json.Json;
import javax.json.stream.JsonGenerator;
import javax.servlet.GenericServlet;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.sling.api.SlingConstants;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.request.header.MediaRangeList;
import org.apache.sling.api.request.RequestProgressTracker;
import org.apache.sling.api.request.ResponseUtil;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The <code>DefaultErrorHandlerServlet</code>
*
* This is the default error handler servlet registered at the end of the
* global search path
*/
@SuppressWarnings("serial")
@Component(service = Servlet.class,
property = {
Constants.SERVICE_VENDOR + "=The Apache Software Foundation",
"sling.servlet.paths=sling/servlet/errorhandler/default",
"sling.servlet.prefix=-1"
})
public class DefaultErrorHandlerServlet extends GenericServlet {
private static final String JSON_CONTENT_TYPE = "application/json";
private static final String HTML_CONTENT_TYPE = "text/html";
/** default log */
private final transient Logger log = LoggerFactory.getLogger(DefaultErrorHandlerServlet.class);
@Override
public void service(ServletRequest req, ServletResponse res)
throws IOException {
// get settings
Integer scObject = (Integer) req.getAttribute(SlingConstants.ERROR_STATUS);
String statusMessage = (String) req.getAttribute(SlingConstants.ERROR_MESSAGE);
String requestUri = (String) req.getAttribute(SlingConstants.ERROR_REQUEST_URI);
String servletName = (String) req.getAttribute(SlingConstants.ERROR_SERVLET_NAME);
// ensure values
int statusCode = (scObject != null)
? scObject.intValue()
: HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
if (statusMessage == null) {
statusMessage = statusToString(statusCode);
}
//properly consider the 'Accept' header conditions to decide whether to send json or html back
if (req instanceof HttpServletRequest &&
JSON_CONTENT_TYPE.equals(new MediaRangeList((HttpServletRequest)req).prefer(HTML_CONTENT_TYPE, JSON_CONTENT_TYPE))) {
renderJson(req, res, statusMessage, requestUri, servletName, statusCode);
} else {
//default to HTML rendering
renderHtml(req, res, statusMessage, requestUri, servletName, statusCode);
}
}
/**
* Render the error as html
*/
protected void renderHtml(ServletRequest req, ServletResponse res, String statusMessage, String requestUri,
String servletName, int statusCode) throws IOException {
// start the response message
final PrintWriter pw = sendIntro((HttpServletResponse) res, statusCode,
statusMessage, requestUri, servletName);
// write the exception message
final PrintWriter escapingWriter = new PrintWriter(
ResponseUtil.getXmlEscapingWriter(pw));
// dump the stack trace
if (req.getAttribute(SlingConstants.ERROR_EXCEPTION) instanceof Throwable) {
final Throwable throwable = (Throwable) req.getAttribute(SlingConstants.ERROR_EXCEPTION);
pw.println("<h3>Exception:</h3>");
pw.println("<pre>");
pw.flush();
printStackTrace(escapingWriter, throwable);
escapingWriter.flush();
pw.println("</pre>");
}
// dump the request progress tracker
if (req instanceof SlingHttpServletRequest) {
final RequestProgressTracker tracker = ((SlingHttpServletRequest) req).getRequestProgressTracker();
pw.println("<h3>Request Progress:</h3>");
pw.println("<pre>");
pw.flush();
tracker.dump(escapingWriter);
escapingWriter.flush();
pw.println("</pre>");
}
// conclude the response message
sendEpilogue(pw);
}
/**
* Render the error as json
*/
protected void renderJson(ServletRequest req, ServletResponse res, String statusMessage, String requestUri,
String servletName, int statusCode) throws IOException {
HttpServletResponse response = (HttpServletResponse)res;
if (!response.isCommitted()) {
response.reset();
response.setStatus(statusCode);
response.setContentType(JSON_CONTENT_TYPE);
response.setCharacterEncoding("UTF-8");
} else {
// Response already committed: don't change status, but report
// the error inline and warn about that
log.warn("Response already committed, unable to change status, output might not be well formed");
}
// send the error as JSON
try (JsonGenerator jsonGenerator = Json.createGenerator(res.getWriter())) {
jsonGenerator.writeStartObject();
jsonGenerator.write("status", statusCode);
String msg = (String)req.getAttribute(SlingConstants.ERROR_MESSAGE);
if (msg != null && !msg.isEmpty()) {
jsonGenerator.write("message", statusMessage);
}
if (requestUri != null && !requestUri.isEmpty()) {
jsonGenerator.write("requestUri", requestUri);
}
if (servletName != null && !servletName.isEmpty()) {
jsonGenerator.write("servletName", servletName);
}
// SLING-10615 - for backward compatibility check for either a
// String or Class value
Object exceptionTypeObj = req.getAttribute(SlingConstants.ERROR_EXCEPTION_TYPE);
String exceptionType = null;
if (exceptionTypeObj instanceof String) {
exceptionType = (String)exceptionTypeObj;
} else if (exceptionTypeObj instanceof Class) {
exceptionType = ((Class<?>)exceptionTypeObj).getName();
}
if (exceptionType != null && !exceptionType.isEmpty()) {
jsonGenerator.write("exceptionType", exceptionType);
}
// dump the stack trace
if (req.getAttribute(SlingConstants.ERROR_EXCEPTION) instanceof Throwable) {
final Throwable throwable = (Throwable) req.getAttribute(SlingConstants.ERROR_EXCEPTION);
try (StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw)) {
printStackTrace(pw, throwable);
jsonGenerator.write("exception", sw.toString());
}
}
// dump the request progress tracker
if (req instanceof SlingHttpServletRequest) {
// dump the request progress tracker
final RequestProgressTracker tracker = ((SlingHttpServletRequest)req).getRequestProgressTracker();
StringWriter strWriter = new StringWriter();
try (PrintWriter progressWriter = new PrintWriter(strWriter)) {
tracker.dump(progressWriter);
}
jsonGenerator.write("requestProgress", strWriter.toString());
}
jsonGenerator.writeEnd();
}
}
/**
* Print the stack trace for the root exception if the throwable is a
* {@link ServletException}. If this does not contain an exception,
* the throwable itself is printed.
*/
private void printStackTrace(PrintWriter pw, Throwable t) {
// nothing to do, if there is no exception
if (t == null) {
return;
}
// unpack a servlet exception
if (t instanceof ServletException) {
ServletException se = (ServletException) t;
while (se.getRootCause() != null) {
t = se.getRootCause();
if (t instanceof ServletException) {
se = (ServletException) t;
} else {
break;
}
}
}
// dump stack, including causes
t.printStackTrace(pw);
}
/**
* Sets the response status and content type header and starts the the
* response HTML text with the header, and an introductory phrase.
*/
private PrintWriter sendIntro(final HttpServletResponse response,
final int statusCode,
final String statusMessageIn,
final String requestUri,
final String servletName)
throws IOException {
final String statusMessage = ResponseUtil.escapeXml(statusMessageIn);
// set the status code and content type in the response
final PrintWriter pw;
if (!response.isCommitted()) {
response.reset();
response.setStatus(statusCode);
response.setContentType(HTML_CONTENT_TYPE);
response.setCharacterEncoding("UTF-8");
pw = response.getWriter();
pw.println("<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">");
pw.println("<html>");
pw.println("<head>");
pw.print("<title>");
pw.print(statusCode);
pw.print(" ");
pw.print(statusMessage);
pw.println("</title>");
pw.println("</head>");
pw.println("<body>");
} else {
// Response already committed: don't change status or write HTML prolog, but report
// the error inline and warn about that
log.warn("Response already committed, unable to change status, output might not be well formed");
pw = response.getWriter();
}
pw.print("<h1>");
pw.print(statusMessage);
pw.print(" (");
pw.print(statusCode);
pw.println(")</h1>");
pw.print("<p>The requested URL ");
pw.print(ResponseUtil.escapeXml(requestUri));
pw.print(" resulted in an error");
if (servletName != null) {
pw.print(" in ");
pw.print(ResponseUtil.escapeXml(servletName));
}
pw.println(".</p>");
return pw;
}
/**
* Ends the response sending with an apache-style server line and closes the
* body and html tags of the HTML response text.
*/
private void sendEpilogue(final PrintWriter pw) {
pw.println("<hr>");
pw.print("<address>");
pw.print(ResponseUtil.escapeXml(getServletContext().getServerInfo()));
pw.println("</address>");
pw.println("</body>");
pw.println("</html>");
}
public static String statusToString(int statusCode) {
switch (statusCode) { // NOSONAR
case 100:
return "Continue";
case 101:
return "Switching Protocols";
case 102:
return "Processing (WebDAV)";
case 200:
return "OK";
case 201:
return "Created";
case 202:
return "Accepted";
case 203:
return "Non-Authoritative Information";
case 204:
return "No Content";
case 205:
return "Reset Content";
case 206:
return "Partial Content";
case 207:
return "Multi-Status (WebDAV)";
case 300:
return "Multiple Choices";
case 301:
return "Moved Permanently";
case 302:
return "Found";
case 303:
return "See Other";
case 304:
return "Not Modified";
case 305:
return "Use Proxy";
case 307:
return "Temporary Redirect";
case 400:
return "Bad Request";
case 401:
return "Unauthorized";
case 402:
return "Payment Required";
case 403:
return "Forbidden";
case 404:
return "Not Found";
case 405:
return "Method Not Allowed";
case 406:
return "Not Acceptable";
case 407:
return "Proxy Authentication Required";
case 408:
return "Request Time-out";
case 409:
return "Conflict";
case 410:
return "Gone";
case 411:
return "Length Required";
case 412:
return "Precondition Failed";
case 413:
return "Request Entity Too Large";
case 414:
return "Request-URI Too Large";
case 415:
return "Unsupported Media Type";
case 416:
return "Requested range not satisfiable";
case 417:
return "Expectation Failed";
case 422:
return "Unprocessable Entity (WebDAV)";
case 423:
return "Locked (WebDAV)";
case 424:
return "Failed Dependency (WebDAV)";
case 500:
return "Internal Server Error";
case 501:
return "Not Implemented";
case 502:
return "Bad Gateway";
case 503:
return "Service Unavailable";
case 504:
return "Gateway Time-out";
case 505:
return "HTTP Version not supported";
case 507:
return "Insufficient Storage (WebDAV)";
case 510:
return "Not Extended";
default:
return String.valueOf(statusCode);
}
}
}