Skip to content

Commit 91ec274

Browse files
jerzykrlkrstoyanchev
authored andcommitted
SPR-17130 http error details in the exception message
1 parent 8bb165e commit 91ec274

10 files changed

+512
-65
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package org.springframework.web.client;
2+
3+
import java.io.ByteArrayInputStream;
4+
import java.io.IOException;
5+
import java.io.InputStreamReader;
6+
import java.io.Reader;
7+
import java.net.URI;
8+
import java.nio.CharBuffer;
9+
import java.nio.charset.Charset;
10+
import java.nio.charset.StandardCharsets;
11+
12+
import org.jetbrains.annotations.NotNull;
13+
import org.springframework.http.HttpMethod;
14+
import org.springframework.lang.Nullable;
15+
16+
/**
17+
* Spring's default implementation of the {@link HttpErrorDetailsExtractor} interface.
18+
*
19+
* <p>This extractor will compose a short summary of the http error response, including:
20+
* <ul>
21+
* <li>request URI
22+
* <li>request method
23+
* <li>a 200-character preview of the response body, unformatted
24+
* </ul>
25+
*
26+
* An example:
27+
* <pre>
28+
* 404 Not Found after GET http://example.com:8080/my-endpoint : [{'id': 123, 'message': 'my very long... (500 bytes)]</code>
29+
* </pre>
30+
*
31+
* @author Jerzy Krolak
32+
* @since 5.1
33+
* @see DefaultResponseErrorHandler#setHttpErrorDetailsExtractor(HttpErrorDetailsExtractor)
34+
*/
35+
public class DefaultHttpErrorDetailsExtractor implements HttpErrorDetailsExtractor {
36+
37+
private static final int MAX_BODY_BYTES_LENGTH = 400;
38+
39+
private static final int MAX_BODY_CHARS_LENGTH = 200;
40+
41+
/**
42+
* Assemble a short summary of the HTTP error response.
43+
* @param rawStatusCode HTTP status code
44+
* @param statusText HTTP status text
45+
* @param responseBody response body
46+
* @param responseCharset response charset
47+
* @param url request URI
48+
* @param method request method
49+
* @return error details string. Example: <pre>404 Not Found after GET http://example.com:8080/my-endpoint : [{'id': 123, 'message': 'my very long... (500 bytes)]</code></pre>
50+
*/
51+
@Override
52+
@NotNull
53+
public String getErrorDetails(int rawStatusCode, String statusText, @Nullable byte[] responseBody,
54+
@Nullable Charset responseCharset, @Nullable URI url, @Nullable HttpMethod method) {
55+
56+
if (url == null || method == null) {
57+
return getSimpleErrorDetails(rawStatusCode, statusText);
58+
}
59+
60+
return getCompleteErrorDetails(rawStatusCode, statusText, responseBody, responseCharset, url, method);
61+
}
62+
63+
@NotNull
64+
private String getCompleteErrorDetails(int rawStatusCode, String statusText, @Nullable byte[] responseBody,
65+
@Nullable Charset responseCharset, @Nullable URI url, @Nullable HttpMethod method) {
66+
67+
StringBuilder result = new StringBuilder();
68+
69+
result.append(getSimpleErrorDetails(rawStatusCode, statusText))
70+
.append(" after ")
71+
.append(method)
72+
.append(" ")
73+
.append(url)
74+
.append(" : ");
75+
76+
if (responseBody == null || responseBody.length == 0) {
77+
result.append("[no body]");
78+
}
79+
else {
80+
result
81+
.append("[")
82+
.append(getResponseBody(responseBody, responseCharset))
83+
.append("]");
84+
}
85+
86+
return result.toString();
87+
}
88+
89+
@NotNull
90+
private String getSimpleErrorDetails(int rawStatusCode, String statusText) {
91+
return rawStatusCode + " " + statusText;
92+
}
93+
94+
private String getResponseBody(byte[] responseBody, @Nullable Charset responseCharset) {
95+
Charset charset = getCharsetOrDefault(responseCharset);
96+
if (responseBody.length < MAX_BODY_BYTES_LENGTH) {
97+
return getCompleteResponseBody(responseBody, charset);
98+
}
99+
return getResponseBodyPreview(responseBody, charset);
100+
}
101+
102+
@NotNull
103+
private String getCompleteResponseBody(byte[] responseBody, Charset responseCharset) {
104+
return new String(responseBody, responseCharset);
105+
}
106+
107+
private String getResponseBodyPreview(byte[] responseBody, Charset responseCharset) {
108+
try {
109+
String bodyPreview = readBodyAsString(responseBody, responseCharset);
110+
return bodyPreview + "... (" + responseBody.length + " bytes)";
111+
}
112+
catch (IOException e) {
113+
// should never happen
114+
throw new IllegalStateException(e);
115+
}
116+
}
117+
118+
@NotNull
119+
private String readBodyAsString(byte[] responseBody, Charset responseCharset) throws IOException {
120+
121+
Reader reader = new InputStreamReader(new ByteArrayInputStream(responseBody), responseCharset);
122+
CharBuffer result = CharBuffer.allocate(MAX_BODY_CHARS_LENGTH);
123+
124+
reader.read(result);
125+
reader.close();
126+
result.flip();
127+
128+
return result.toString();
129+
}
130+
131+
private Charset getCharsetOrDefault(@Nullable Charset responseCharset) {
132+
if (responseCharset == null) {
133+
return StandardCharsets.ISO_8859_1;
134+
}
135+
return responseCharset;
136+
}
137+
138+
}

spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@
1717
package org.springframework.web.client;
1818

1919
import java.io.IOException;
20+
import java.net.URI;
2021
import java.nio.charset.Charset;
2122

2223
import org.springframework.http.HttpHeaders;
24+
import org.springframework.http.HttpMethod;
2325
import org.springframework.http.HttpStatus;
2426
import org.springframework.http.MediaType;
2527
import org.springframework.http.client.ClientHttpResponse;
2628
import org.springframework.lang.Nullable;
29+
import org.springframework.util.Assert;
2730
import org.springframework.util.FileCopyUtils;
2831

2932
/**
@@ -43,6 +46,17 @@
4346
*/
4447
public class DefaultResponseErrorHandler implements ResponseErrorHandler {
4548

49+
private HttpErrorDetailsExtractor httpErrorDetailsExtractor = new DefaultHttpErrorDetailsExtractor();
50+
51+
/**
52+
* Set the error summary extractor.
53+
* <p>By default, DefaultResponseErrorHandler uses a {@link DefaultHttpErrorDetailsExtractor}.
54+
*/
55+
public void setHttpErrorDetailsExtractor(HttpErrorDetailsExtractor httpErrorDetailsExtractor) {
56+
Assert.notNull(httpErrorDetailsExtractor, "HttpErrorDetailsExtractor must not be null");
57+
this.httpErrorDetailsExtractor = httpErrorDetailsExtractor;
58+
}
59+
4660
/**
4761
* Delegates to {@link #hasError(HttpStatus)} (for a standard status enum value) or
4862
* {@link #hasError(int)} (for an unknown status code) with the response status code.
@@ -87,19 +101,31 @@ protected boolean hasError(int unknownStatusCode) {
87101
}
88102

89103
/**
90-
* Delegates to {@link #handleError(ClientHttpResponse, HttpStatus)} with the
104+
* Delegates to {@link #handleError(URI, HttpMethod, ClientHttpResponse, HttpStatus)} with the
91105
* response status code.
92106
* @throws UnknownHttpStatusCodeException in case of an unresolvable status code
93-
* @see #handleError(ClientHttpResponse, HttpStatus)
107+
* @see #handleError(URI, HttpMethod, ClientHttpResponse, HttpStatus)
94108
*/
95109
@Override
96110
public void handleError(ClientHttpResponse response) throws IOException {
111+
handleError(null, null, response);
112+
}
113+
114+
/**
115+
* Delegates to {@link #handleError(URI, HttpMethod, ClientHttpResponse, HttpStatus)} with the
116+
* response status code.
117+
* @throws UnknownHttpStatusCodeException in case of an unresolvable status code
118+
* @see #handleError(URI, HttpMethod, ClientHttpResponse, HttpStatus)
119+
*/
120+
@Override
121+
public void handleError(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {
97122
HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode());
98123
if (statusCode == null) {
99-
throw new UnknownHttpStatusCodeException(response.getRawStatusCode(), response.getStatusText(),
100-
response.getHeaders(), getResponseBody(response), getCharset(response));
124+
String message = httpErrorDetailsExtractor.getErrorDetails(response.getRawStatusCode(), response.getStatusText(), getResponseBody(response), getCharset(response), url, method);
125+
throw new UnknownHttpStatusCodeException(message, response.getRawStatusCode(), response.getStatusText(),
126+
response.getHeaders(), getResponseBody(response), getCharset(response), url, method);
101127
}
102-
handleError(response, statusCode);
128+
handleError(url, method, response, statusCode);
103129
}
104130

105131
/**
@@ -114,17 +140,34 @@ public void handleError(ClientHttpResponse response) throws IOException {
114140
* @see HttpServerErrorException#create
115141
*/
116142
protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
143+
handleError(null, null, response, statusCode);
144+
}
145+
146+
/**
147+
* Handle the error in the given response with the given resolved status code.
148+
* <p>This default implementation throws a {@link HttpClientErrorException} if the response status code
149+
* is {@link org.springframework.http.HttpStatus.Series#CLIENT_ERROR}, a {@link HttpServerErrorException}
150+
* if it is {@link org.springframework.http.HttpStatus.Series#SERVER_ERROR},
151+
* and a {@link RestClientException} in other cases.
152+
* @since 5.0
153+
*/
154+
protected void handleError(@Nullable URI url, @Nullable HttpMethod method, ClientHttpResponse response,
155+
HttpStatus statusCode) throws IOException {
156+
117157
String statusText = response.getStatusText();
118158
HttpHeaders headers = response.getHeaders();
119159
byte[] body = getResponseBody(response);
120160
Charset charset = getCharset(response);
161+
String message = httpErrorDetailsExtractor.getErrorDetails(statusCode.value(), statusText, body, charset, url, method);
162+
121163
switch (statusCode.series()) {
122164
case CLIENT_ERROR:
123-
throw HttpClientErrorException.create(statusCode, statusText, headers, body, charset);
165+
throw HttpClientErrorException.create(message, statusCode, statusText, headers, body, charset, url, method);
124166
case SERVER_ERROR:
125-
throw HttpServerErrorException.create(statusCode, statusText, headers, body, charset);
167+
throw HttpServerErrorException.create(message, statusCode, statusText, headers, body, charset, url, method);
126168
default:
127-
throw new UnknownHttpStatusCodeException(statusCode.value(), statusText, headers, body, charset);
169+
throw new UnknownHttpStatusCodeException(message, statusCode.value(), statusText, headers, body,
170+
charset, url, method);
128171
}
129172
}
130173

0 commit comments

Comments
 (0)