001/* 002 * Copyright 2012-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.web.servlet.error; 018 019import java.io.PrintWriter; 020import java.io.StringWriter; 021import java.util.Date; 022import java.util.LinkedHashMap; 023import java.util.Map; 024 025import javax.servlet.ServletException; 026import javax.servlet.http.HttpServletRequest; 027import javax.servlet.http.HttpServletResponse; 028 029import org.springframework.core.Ordered; 030import org.springframework.core.annotation.Order; 031import org.springframework.http.HttpStatus; 032import org.springframework.util.StringUtils; 033import org.springframework.validation.BindingResult; 034import org.springframework.validation.ObjectError; 035import org.springframework.web.bind.MethodArgumentNotValidException; 036import org.springframework.web.context.request.RequestAttributes; 037import org.springframework.web.context.request.WebRequest; 038import org.springframework.web.servlet.HandlerExceptionResolver; 039import org.springframework.web.servlet.ModelAndView; 040 041/** 042 * Default implementation of {@link ErrorAttributes}. Provides the following attributes 043 * when possible: 044 * <ul> 045 * <li>timestamp - The time that the errors were extracted</li> 046 * <li>status - The status code</li> 047 * <li>error - The error reason</li> 048 * <li>exception - The class name of the root exception (if configured)</li> 049 * <li>message - The exception message</li> 050 * <li>errors - Any {@link ObjectError}s from a {@link BindingResult} exception 051 * <li>trace - The exception stack trace</li> 052 * <li>path - The URL path when the exception was raised</li> 053 * </ul> 054 * 055 * @author Phillip Webb 056 * @author Dave Syer 057 * @author Stephane Nicoll 058 * @author Vedran Pavic 059 * @since 2.0.0 060 * @see ErrorAttributes 061 */ 062@Order(Ordered.HIGHEST_PRECEDENCE) 063public class DefaultErrorAttributes 064 implements ErrorAttributes, HandlerExceptionResolver, Ordered { 065 066 private static final String ERROR_ATTRIBUTE = DefaultErrorAttributes.class.getName() 067 + ".ERROR"; 068 069 private final boolean includeException; 070 071 /** 072 * Create a new {@link DefaultErrorAttributes} instance that does not include the 073 * "exception" attribute. 074 */ 075 public DefaultErrorAttributes() { 076 this(false); 077 } 078 079 /** 080 * Create a new {@link DefaultErrorAttributes} instance. 081 * @param includeException whether to include the "exception" attribute 082 */ 083 public DefaultErrorAttributes(boolean includeException) { 084 this.includeException = includeException; 085 } 086 087 @Override 088 public int getOrder() { 089 return Ordered.HIGHEST_PRECEDENCE; 090 } 091 092 @Override 093 public ModelAndView resolveException(HttpServletRequest request, 094 HttpServletResponse response, Object handler, Exception ex) { 095 storeErrorAttributes(request, ex); 096 return null; 097 } 098 099 private void storeErrorAttributes(HttpServletRequest request, Exception ex) { 100 request.setAttribute(ERROR_ATTRIBUTE, ex); 101 } 102 103 @Override 104 public Map<String, Object> getErrorAttributes(WebRequest webRequest, 105 boolean includeStackTrace) { 106 Map<String, Object> errorAttributes = new LinkedHashMap<>(); 107 errorAttributes.put("timestamp", new Date()); 108 addStatus(errorAttributes, webRequest); 109 addErrorDetails(errorAttributes, webRequest, includeStackTrace); 110 addPath(errorAttributes, webRequest); 111 return errorAttributes; 112 } 113 114 private void addStatus(Map<String, Object> errorAttributes, 115 RequestAttributes requestAttributes) { 116 Integer status = getAttribute(requestAttributes, 117 "javax.servlet.error.status_code"); 118 if (status == null) { 119 errorAttributes.put("status", 999); 120 errorAttributes.put("error", "None"); 121 return; 122 } 123 errorAttributes.put("status", status); 124 try { 125 errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase()); 126 } 127 catch (Exception ex) { 128 // Unable to obtain a reason 129 errorAttributes.put("error", "Http Status " + status); 130 } 131 } 132 133 private void addErrorDetails(Map<String, Object> errorAttributes, 134 WebRequest webRequest, boolean includeStackTrace) { 135 Throwable error = getError(webRequest); 136 if (error != null) { 137 while (error instanceof ServletException && error.getCause() != null) { 138 error = ((ServletException) error).getCause(); 139 } 140 if (this.includeException) { 141 errorAttributes.put("exception", error.getClass().getName()); 142 } 143 addErrorMessage(errorAttributes, error); 144 if (includeStackTrace) { 145 addStackTrace(errorAttributes, error); 146 } 147 } 148 Object message = getAttribute(webRequest, "javax.servlet.error.message"); 149 if ((!StringUtils.isEmpty(message) || errorAttributes.get("message") == null) 150 && !(error instanceof BindingResult)) { 151 errorAttributes.put("message", 152 StringUtils.isEmpty(message) ? "No message available" : message); 153 } 154 } 155 156 private void addErrorMessage(Map<String, Object> errorAttributes, Throwable error) { 157 BindingResult result = extractBindingResult(error); 158 if (result == null) { 159 errorAttributes.put("message", error.getMessage()); 160 return; 161 } 162 if (result.hasErrors()) { 163 errorAttributes.put("errors", result.getAllErrors()); 164 errorAttributes.put("message", 165 "Validation failed for object='" + result.getObjectName() 166 + "'. Error count: " + result.getErrorCount()); 167 } 168 else { 169 errorAttributes.put("message", "No errors"); 170 } 171 } 172 173 private BindingResult extractBindingResult(Throwable error) { 174 if (error instanceof BindingResult) { 175 return (BindingResult) error; 176 } 177 if (error instanceof MethodArgumentNotValidException) { 178 return ((MethodArgumentNotValidException) error).getBindingResult(); 179 } 180 return null; 181 } 182 183 private void addStackTrace(Map<String, Object> errorAttributes, Throwable error) { 184 StringWriter stackTrace = new StringWriter(); 185 error.printStackTrace(new PrintWriter(stackTrace)); 186 stackTrace.flush(); 187 errorAttributes.put("trace", stackTrace.toString()); 188 } 189 190 private void addPath(Map<String, Object> errorAttributes, 191 RequestAttributes requestAttributes) { 192 String path = getAttribute(requestAttributes, "javax.servlet.error.request_uri"); 193 if (path != null) { 194 errorAttributes.put("path", path); 195 } 196 } 197 198 @Override 199 public Throwable getError(WebRequest webRequest) { 200 Throwable exception = getAttribute(webRequest, ERROR_ATTRIBUTE); 201 if (exception == null) { 202 exception = getAttribute(webRequest, "javax.servlet.error.exception"); 203 } 204 return exception; 205 } 206 207 @SuppressWarnings("unchecked") 208 private <T> T getAttribute(RequestAttributes requestAttributes, String name) { 209 return (T) requestAttributes.getAttribute(name, RequestAttributes.SCOPE_REQUEST); 210 } 211 212}