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.client;
018
019import java.io.ByteArrayInputStream;
020import java.io.IOException;
021import java.io.InputStreamReader;
022import java.io.Reader;
023import java.nio.CharBuffer;
024import java.nio.charset.Charset;
025import java.nio.charset.StandardCharsets;
026
027import org.springframework.http.HttpHeaders;
028import org.springframework.http.HttpStatus;
029import org.springframework.http.MediaType;
030import org.springframework.http.client.ClientHttpResponse;
031import org.springframework.lang.Nullable;
032import org.springframework.util.FileCopyUtils;
033import org.springframework.util.ObjectUtils;
034
035/**
036 * Spring's default implementation of the {@link ResponseErrorHandler} interface.
037 *
038 * <p>This error handler checks for the status code on the
039 * {@link ClientHttpResponse}. Any code in the 4xx or 5xx series is considered
040 * to be an error. This behavior can be changed by overriding
041 * {@link #hasError(HttpStatus)}. Unknown status codes will be ignored by
042 * {@link #hasError(ClientHttpResponse)}.
043 *
044 * <p>See {@link #handleError(ClientHttpResponse)} for more details on specific
045 * exception types.
046 *
047 * @author Arjen Poutsma
048 * @author Rossen Stoyanchev
049 * @author Juergen Hoeller
050 * @since 3.0
051 * @see RestTemplate#setErrorHandler
052 */
053public class DefaultResponseErrorHandler implements ResponseErrorHandler {
054
055        /**
056         * Delegates to {@link #hasError(HttpStatus)} (for a standard status enum value) or
057         * {@link #hasError(int)} (for an unknown status code) with the response status code.
058         * @see ClientHttpResponse#getRawStatusCode()
059         * @see #hasError(HttpStatus)
060         * @see #hasError(int)
061         */
062        @Override
063        public boolean hasError(ClientHttpResponse response) throws IOException {
064                int rawStatusCode = response.getRawStatusCode();
065                HttpStatus statusCode = HttpStatus.resolve(rawStatusCode);
066                return (statusCode != null ? hasError(statusCode) : hasError(rawStatusCode));
067        }
068
069        /**
070         * Template method called from {@link #hasError(ClientHttpResponse)}.
071         * <p>The default implementation checks {@link HttpStatus#isError()}.
072         * Can be overridden in subclasses.
073         * @param statusCode the HTTP status code as enum value
074         * @return {@code true} if the response indicates an error; {@code false} otherwise
075         * @see HttpStatus#isError()
076         */
077        protected boolean hasError(HttpStatus statusCode) {
078                return statusCode.isError();
079        }
080
081        /**
082         * Template method called from {@link #hasError(ClientHttpResponse)}.
083         * <p>The default implementation checks if the given status code is
084         * {@link org.springframework.http.HttpStatus.Series#CLIENT_ERROR CLIENT_ERROR} or
085         * {@link org.springframework.http.HttpStatus.Series#SERVER_ERROR SERVER_ERROR}.
086         * Can be overridden in subclasses.
087         * @param unknownStatusCode the HTTP status code as raw value
088         * @return {@code true} if the response indicates an error; {@code false} otherwise
089         * @since 4.3.21
090         * @see org.springframework.http.HttpStatus.Series#CLIENT_ERROR
091         * @see org.springframework.http.HttpStatus.Series#SERVER_ERROR
092         */
093        protected boolean hasError(int unknownStatusCode) {
094                HttpStatus.Series series = HttpStatus.Series.resolve(unknownStatusCode);
095                return (series == HttpStatus.Series.CLIENT_ERROR || series == HttpStatus.Series.SERVER_ERROR);
096        }
097
098        /**
099         * Handle the error in the given response with the given resolved status code.
100         * <p>The default implementation throws:
101         * <ul>
102         * <li>{@link HttpClientErrorException} if the status code is in the 4xx
103         * series, or one of its sub-classes such as
104         * {@link HttpClientErrorException.BadRequest} and others.
105         * <li>{@link HttpServerErrorException} if the status code is in the 5xx
106         * series, or one of its sub-classes such as
107         * {@link HttpServerErrorException.InternalServerError} and others.
108         * <li>{@link UnknownHttpStatusCodeException} for error status codes not in the
109         * {@link HttpStatus} enum range.
110         * </ul>
111         * @throws UnknownHttpStatusCodeException in case of an unresolvable status code
112         * @see #handleError(ClientHttpResponse, HttpStatus)
113         */
114        @Override
115        public void handleError(ClientHttpResponse response) throws IOException {
116                HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode());
117                if (statusCode == null) {
118                        byte[] body = getResponseBody(response);
119                        String message = getErrorMessage(response.getRawStatusCode(),
120                                        response.getStatusText(), body, getCharset(response));
121                        throw new UnknownHttpStatusCodeException(message,
122                                        response.getRawStatusCode(), response.getStatusText(),
123                                        response.getHeaders(), body, getCharset(response));
124                }
125                handleError(response, statusCode);
126        }
127
128        /**
129         * Return error message with details from the response body, possibly truncated:
130         * <pre>
131         * 404 Not Found: [{'id': 123, 'message': 'my very long... (500 bytes)]
132         * </pre>
133         */
134        private String getErrorMessage(
135                        int rawStatusCode, String statusText, @Nullable byte[] responseBody, @Nullable Charset charset) {
136
137                String preface = rawStatusCode + " " + statusText + ": ";
138                if (ObjectUtils.isEmpty(responseBody)) {
139                        return preface + "[no body]";
140                }
141
142                charset = charset == null ? StandardCharsets.UTF_8 : charset;
143                int maxChars = 200;
144
145                if (responseBody.length < maxChars * 2) {
146                        return preface + "[" + new String(responseBody, charset) + "]";
147                }
148
149                try {
150                        Reader reader = new InputStreamReader(new ByteArrayInputStream(responseBody), charset);
151                        CharBuffer buffer = CharBuffer.allocate(maxChars);
152                        reader.read(buffer);
153                        reader.close();
154                        buffer.flip();
155                        return preface + "[" + buffer.toString() + "... (" + responseBody.length + " bytes)]";
156                }
157                catch (IOException ex) {
158                        // should never happen
159                        throw new IllegalStateException(ex);
160                }
161        }
162
163        /**
164         * Handle the error based on the resolved status code.
165         *
166         * <p>The default implementation delegates to
167         * {@link HttpClientErrorException#create} for errors in the 4xx range, to
168         * {@link HttpServerErrorException#create} for errors in the 5xx range,
169         * or otherwise raises {@link UnknownHttpStatusCodeException}.
170         *
171         * @since 5.0
172         * @see HttpClientErrorException#create
173         * @see HttpServerErrorException#create
174         */
175        protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
176                String statusText = response.getStatusText();
177                HttpHeaders headers = response.getHeaders();
178                byte[] body = getResponseBody(response);
179                Charset charset = getCharset(response);
180                String message = getErrorMessage(statusCode.value(), statusText, body, charset);
181
182                switch (statusCode.series()) {
183                        case CLIENT_ERROR:
184                                throw HttpClientErrorException.create(message, statusCode, statusText, headers, body, charset);
185                        case SERVER_ERROR:
186                                throw HttpServerErrorException.create(message, statusCode, statusText, headers, body, charset);
187                        default:
188                                throw new UnknownHttpStatusCodeException(message, statusCode.value(), statusText, headers, body, charset);
189                }
190        }
191
192        /**
193         * Determine the HTTP status of the given response.
194         * @param response the response to inspect
195         * @return the associated HTTP status
196         * @throws IOException in case of I/O errors
197         * @throws UnknownHttpStatusCodeException in case of an unknown status code
198         * that cannot be represented with the {@link HttpStatus} enum
199         * @since 4.3.8
200         * @deprecated as of 5.0, in favor of {@link #handleError(ClientHttpResponse, HttpStatus)}
201         */
202        @Deprecated
203        protected HttpStatus getHttpStatusCode(ClientHttpResponse response) throws IOException {
204                HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode());
205                if (statusCode == null) {
206                        throw new UnknownHttpStatusCodeException(response.getRawStatusCode(), response.getStatusText(),
207                                        response.getHeaders(), getResponseBody(response), getCharset(response));
208                }
209                return statusCode;
210        }
211
212        /**
213         * Read the body of the given response (for inclusion in a status exception).
214         * @param response the response to inspect
215         * @return the response body as a byte array,
216         * or an empty byte array if the body could not be read
217         * @since 4.3.8
218         */
219        protected byte[] getResponseBody(ClientHttpResponse response) {
220                try {
221                        return FileCopyUtils.copyToByteArray(response.getBody());
222                }
223                catch (IOException ex) {
224                        // ignore
225                }
226                return new byte[0];
227        }
228
229        /**
230         * Determine the charset of the response (for inclusion in a status exception).
231         * @param response the response to inspect
232         * @return the associated charset, or {@code null} if none
233         * @since 4.3.8
234         */
235        @Nullable
236        protected Charset getCharset(ClientHttpResponse response) {
237                HttpHeaders headers = response.getHeaders();
238                MediaType contentType = headers.getContentType();
239                return (contentType != null ? contentType.getCharset() : null);
240        }
241
242}