001/*
002 * Copyright 2002-2020 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 *      https://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.web.server.handler;
018
019import org.apache.commons.logging.Log;
020import org.apache.commons.logging.LogFactory;
021import reactor.core.publisher.Mono;
022
023import org.springframework.http.HttpStatus;
024import org.springframework.http.server.reactive.ServerHttpRequest;
025import org.springframework.http.server.reactive.ServerHttpResponse;
026import org.springframework.lang.Nullable;
027import org.springframework.web.server.ResponseStatusException;
028import org.springframework.web.server.ServerWebExchange;
029import org.springframework.web.server.WebExceptionHandler;
030
031/**
032 * Handle {@link ResponseStatusException} by setting the response status.
033 *
034 * <p>By default exception stack traces are not shown for successfully resolved
035 * exceptions. Use {@link #setWarnLogCategory(String)} to enable logging with
036 * stack traces.
037 *
038 * @author Rossen Stoyanchev
039 * @author Sebastien Deleuze
040 * @since 5.0
041 */
042public class ResponseStatusExceptionHandler implements WebExceptionHandler {
043
044        private static final Log logger = LogFactory.getLog(ResponseStatusExceptionHandler.class);
045
046
047        @Nullable
048        private Log warnLogger;
049
050
051        /**
052         * Set the log category for warn logging.
053         * <p>Default is no warn logging. Specify this setting to activate warn
054         * logging into a specific category.
055         * @since 5.1
056         * @see org.apache.commons.logging.LogFactory#getLog(String)
057         * @see java.util.logging.Logger#getLogger(String)
058         */
059        public void setWarnLogCategory(String loggerName) {
060                this.warnLogger = LogFactory.getLog(loggerName);
061        }
062
063
064        @Override
065        public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
066                if (!updateResponse(exchange.getResponse(), ex)) {
067                        return Mono.error(ex);
068                }
069
070                // Mirrors AbstractHandlerExceptionResolver in spring-webmvc...
071                String logPrefix = exchange.getLogPrefix();
072                if (this.warnLogger != null && this.warnLogger.isWarnEnabled()) {
073                        this.warnLogger.warn(logPrefix + formatError(ex, exchange.getRequest()), ex);
074                }
075                else if (logger.isDebugEnabled()) {
076                        logger.debug(logPrefix + formatError(ex, exchange.getRequest()));
077                }
078
079                return exchange.getResponse().setComplete();
080        }
081
082
083        private String formatError(Throwable ex, ServerHttpRequest request) {
084                String reason = ex.getClass().getSimpleName() + ": " + ex.getMessage();
085                String path = request.getURI().getRawPath();
086                return "Resolved [" + reason + "] for HTTP " + request.getMethod() + " " + path;
087        }
088
089        private boolean updateResponse(ServerHttpResponse response, Throwable ex) {
090                boolean result = false;
091                HttpStatus status = determineStatus(ex);
092                if (status != null) {
093                        if (response.setStatusCode(status)) {
094                                if (ex instanceof ResponseStatusException) {
095                                        ((ResponseStatusException) ex).getResponseHeaders()
096                                                        .forEach((name, values) ->
097                                                                        values.forEach(value -> response.getHeaders().add(name, value)));
098                                }
099                                result = true;
100                        }
101                }
102                else {
103                        Throwable cause = ex.getCause();
104                        if (cause != null) {
105                                result = updateResponse(response, cause);
106                        }
107                }
108                return result;
109        }
110
111        /**
112         * Determine the HTTP status implied by the given exception.
113         * @param ex the exception to introspect
114         * @return the associated HTTP status, if any
115         * @since 5.0.5
116         */
117        @Nullable
118        protected HttpStatus determineStatus(Throwable ex) {
119                if (ex instanceof ResponseStatusException) {
120                        return ((ResponseStatusException) ex).getStatus();
121                }
122                return null;
123        }
124
125}