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.reactive.error; 018 019import java.io.PrintWriter; 020import java.io.StringWriter; 021import java.util.Date; 022import java.util.LinkedHashMap; 023import java.util.Map; 024 025import org.springframework.core.annotation.AnnotatedElementUtils; 026import org.springframework.http.HttpStatus; 027import org.springframework.validation.BindingResult; 028import org.springframework.validation.ObjectError; 029import org.springframework.web.bind.annotation.ResponseStatus; 030import org.springframework.web.bind.support.WebExchangeBindException; 031import org.springframework.web.reactive.function.server.ServerRequest; 032import org.springframework.web.server.ResponseStatusException; 033import org.springframework.web.server.ServerWebExchange; 034 035/** 036 * Default implementation of {@link ErrorAttributes}. Provides the following attributes 037 * when possible: 038 * <ul> 039 * <li>timestamp - The time that the errors were extracted</li> 040 * <li>status - The status code</li> 041 * <li>error - The error reason</li> 042 * <li>exception - The class name of the root exception (if configured)</li> 043 * <li>message - The exception message</li> 044 * <li>errors - Any {@link ObjectError}s from a {@link BindingResult} exception 045 * <li>trace - The exception stack trace</li> 046 * <li>path - The URL path when the exception was raised</li> 047 * </ul> 048 * 049 * @author Brian Clozel 050 * @author Stephane Nicoll 051 * @author Michele Mancioppi 052 * @since 2.0.0 053 * @see ErrorAttributes 054 */ 055public class DefaultErrorAttributes implements ErrorAttributes { 056 057 private static final String ERROR_ATTRIBUTE = DefaultErrorAttributes.class.getName() 058 + ".ERROR"; 059 060 private final boolean includeException; 061 062 /** 063 * Create a new {@link DefaultErrorAttributes} instance that does not include the 064 * "exception" attribute. 065 */ 066 public DefaultErrorAttributes() { 067 this(false); 068 } 069 070 /** 071 * Create a new {@link DefaultErrorAttributes} instance. 072 * @param includeException whether to include the "exception" attribute 073 */ 074 public DefaultErrorAttributes(boolean includeException) { 075 this.includeException = includeException; 076 } 077 078 @Override 079 public Map<String, Object> getErrorAttributes(ServerRequest request, 080 boolean includeStackTrace) { 081 Map<String, Object> errorAttributes = new LinkedHashMap<>(); 082 errorAttributes.put("timestamp", new Date()); 083 errorAttributes.put("path", request.path()); 084 Throwable error = getError(request); 085 HttpStatus errorStatus = determineHttpStatus(error); 086 errorAttributes.put("status", errorStatus.value()); 087 errorAttributes.put("error", errorStatus.getReasonPhrase()); 088 errorAttributes.put("message", determineMessage(error)); 089 handleException(errorAttributes, determineException(error), includeStackTrace); 090 return errorAttributes; 091 } 092 093 private HttpStatus determineHttpStatus(Throwable error) { 094 if (error instanceof ResponseStatusException) { 095 return ((ResponseStatusException) error).getStatus(); 096 } 097 ResponseStatus responseStatus = AnnotatedElementUtils 098 .findMergedAnnotation(error.getClass(), ResponseStatus.class); 099 if (responseStatus != null) { 100 return responseStatus.code(); 101 } 102 return HttpStatus.INTERNAL_SERVER_ERROR; 103 } 104 105 private String determineMessage(Throwable error) { 106 if (error instanceof WebExchangeBindException) { 107 return error.getMessage(); 108 } 109 if (error instanceof ResponseStatusException) { 110 return ((ResponseStatusException) error).getReason(); 111 } 112 ResponseStatus responseStatus = AnnotatedElementUtils 113 .findMergedAnnotation(error.getClass(), ResponseStatus.class); 114 if (responseStatus != null) { 115 return responseStatus.reason(); 116 } 117 return error.getMessage(); 118 } 119 120 private Throwable determineException(Throwable error) { 121 if (error instanceof ResponseStatusException) { 122 return (error.getCause() != null) ? error.getCause() : error; 123 } 124 return error; 125 } 126 127 private void addStackTrace(Map<String, Object> errorAttributes, Throwable error) { 128 StringWriter stackTrace = new StringWriter(); 129 error.printStackTrace(new PrintWriter(stackTrace)); 130 stackTrace.flush(); 131 errorAttributes.put("trace", stackTrace.toString()); 132 } 133 134 private void handleException(Map<String, Object> errorAttributes, Throwable error, 135 boolean includeStackTrace) { 136 if (this.includeException) { 137 errorAttributes.put("exception", error.getClass().getName()); 138 } 139 if (includeStackTrace) { 140 addStackTrace(errorAttributes, error); 141 } 142 if (error instanceof BindingResult) { 143 BindingResult result = (BindingResult) error; 144 if (result.hasErrors()) { 145 errorAttributes.put("errors", result.getAllErrors()); 146 } 147 } 148 } 149 150 @Override 151 public Throwable getError(ServerRequest request) { 152 return (Throwable) request.attribute(ERROR_ATTRIBUTE) 153 .orElseThrow(() -> new IllegalStateException( 154 "Missing exception attribute in ServerWebExchange")); 155 } 156 157 @Override 158 public void storeErrorInformation(Throwable error, ServerWebExchange exchange) { 159 exchange.getAttributes().putIfAbsent(ERROR_ATTRIBUTE, error); 160 } 161 162}