Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error details in RestTemplate client and server exceptions #1956

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,138 @@
package org.springframework.web.client;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

import org.jetbrains.annotations.NotNull;
import org.springframework.http.HttpMethod;
import org.springframework.lang.Nullable;

/**
* Spring's default implementation of the {@link HttpErrorDetailsExtractor} interface.
*
* <p>This extractor will compose a short summary of the http error response, including:
* <ul>
* <li>request URI
* <li>request method
* <li>a 200-character preview of the response body, unformatted
* </ul>
*
* An example:
* <pre>
* 404 Not Found after GET http://example.com:8080/my-endpoint : [{'id': 123, 'message': 'my very long... (500 bytes)]</code>
* </pre>
*
* @author Jerzy Krolak
* @since 5.1
* @see DefaultResponseErrorHandler#setHttpErrorDetailsExtractor(HttpErrorDetailsExtractor)
*/
public class DefaultHttpErrorDetailsExtractor implements HttpErrorDetailsExtractor {

private static final int MAX_BODY_BYTES_LENGTH = 400;

private static final int MAX_BODY_CHARS_LENGTH = 200;

/**
* Assemble a short summary of the HTTP error response.
* @param rawStatusCode HTTP status code
* @param statusText HTTP status text
* @param responseBody response body
* @param responseCharset response charset
* @param url request URI
* @param method request method
* @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>
*/
@Override
@NotNull
public String getErrorDetails(int rawStatusCode, String statusText, @Nullable byte[] responseBody,
@Nullable Charset responseCharset, @Nullable URI url, @Nullable HttpMethod method) {

if (url == null || method == null) {
return getSimpleErrorDetails(rawStatusCode, statusText);
}

return getCompleteErrorDetails(rawStatusCode, statusText, responseBody, responseCharset, url, method);
}

@NotNull
private String getCompleteErrorDetails(int rawStatusCode, String statusText, @Nullable byte[] responseBody,
@Nullable Charset responseCharset, @Nullable URI url, @Nullable HttpMethod method) {

StringBuilder result = new StringBuilder();

result.append(getSimpleErrorDetails(rawStatusCode, statusText))
.append(" after ")
.append(method)
.append(" ")
.append(url)
.append(" : ");

if (responseBody == null || responseBody.length == 0) {
result.append("[no body]");
}
else {
result
.append("[")
.append(getResponseBody(responseBody, responseCharset))
.append("]");
}

return result.toString();
}

@NotNull
private String getSimpleErrorDetails(int rawStatusCode, String statusText) {
return rawStatusCode + " " + statusText;
}

private String getResponseBody(byte[] responseBody, @Nullable Charset responseCharset) {
Charset charset = getCharsetOrDefault(responseCharset);
if (responseBody.length < MAX_BODY_BYTES_LENGTH) {
return getCompleteResponseBody(responseBody, charset);
}
return getResponseBodyPreview(responseBody, charset);
}

@NotNull
private String getCompleteResponseBody(byte[] responseBody, Charset responseCharset) {
return new String(responseBody, responseCharset);
}

private String getResponseBodyPreview(byte[] responseBody, Charset responseCharset) {
try {
String bodyPreview = readBodyAsString(responseBody, responseCharset);
return bodyPreview + "... (" + responseBody.length + " bytes)";
}
catch (IOException e) {
// should never happen
throw new IllegalStateException(e);
}
}

@NotNull
private String readBodyAsString(byte[] responseBody, Charset responseCharset) throws IOException {

Reader reader = new InputStreamReader(new ByteArrayInputStream(responseBody), responseCharset);
CharBuffer result = CharBuffer.allocate(MAX_BODY_CHARS_LENGTH);

reader.read(result);
reader.close();
result.flip();

return result.toString();
}

private Charset getCharsetOrDefault(@Nullable Charset responseCharset) {
if (responseCharset == null) {
return StandardCharsets.ISO_8859_1;
}
return responseCharset;
}

}
Expand Up @@ -17,13 +17,16 @@
package org.springframework.web.client;

import java.io.IOException;
import java.net.URI;
import java.nio.charset.Charset;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;

/**
Expand All @@ -43,6 +46,17 @@
*/
public class DefaultResponseErrorHandler implements ResponseErrorHandler {

private HttpErrorDetailsExtractor httpErrorDetailsExtractor = new DefaultHttpErrorDetailsExtractor();

/**
* Set the error summary extractor.
* <p>By default, DefaultResponseErrorHandler uses a {@link DefaultHttpErrorDetailsExtractor}.
*/
public void setHttpErrorDetailsExtractor(HttpErrorDetailsExtractor httpErrorDetailsExtractor) {
Assert.notNull(httpErrorDetailsExtractor, "HttpErrorDetailsExtractor must not be null");
this.httpErrorDetailsExtractor = httpErrorDetailsExtractor;
}

/**
* Delegates to {@link #hasError(HttpStatus)} (for a standard status enum value) or
* {@link #hasError(int)} (for an unknown status code) with the response status code.
Expand Down Expand Up @@ -87,19 +101,31 @@ protected boolean hasError(int unknownStatusCode) {
}

/**
* Delegates to {@link #handleError(ClientHttpResponse, HttpStatus)} with the
* Delegates to {@link #handleError(URI, HttpMethod, ClientHttpResponse, HttpStatus)} with the
* response status code.
* @throws UnknownHttpStatusCodeException in case of an unresolvable status code
* @see #handleError(ClientHttpResponse, HttpStatus)
* @see #handleError(URI, HttpMethod, ClientHttpResponse, HttpStatus)
*/
@Override
public void handleError(ClientHttpResponse response) throws IOException {
handleError(null, null, response);
}

/**
* Delegates to {@link #handleError(URI, HttpMethod, ClientHttpResponse, HttpStatus)} with the
* response status code.
* @throws UnknownHttpStatusCodeException in case of an unresolvable status code
* @see #handleError(URI, HttpMethod, ClientHttpResponse, HttpStatus)
*/
@Override
public void handleError(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {
HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode());
if (statusCode == null) {
throw new UnknownHttpStatusCodeException(response.getRawStatusCode(), response.getStatusText(),
response.getHeaders(), getResponseBody(response), getCharset(response));
String message = httpErrorDetailsExtractor.getErrorDetails(response.getRawStatusCode(), response.getStatusText(), getResponseBody(response), getCharset(response), url, method);
throw new UnknownHttpStatusCodeException(message, response.getRawStatusCode(), response.getStatusText(),
response.getHeaders(), getResponseBody(response), getCharset(response), url, method);
}
handleError(response, statusCode);
handleError(url, method, response, statusCode);
}

/**
Expand All @@ -114,17 +140,34 @@ public void handleError(ClientHttpResponse response) throws IOException {
* @see HttpServerErrorException#create
*/
protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
handleError(null, null, response, statusCode);
}

/**
* Handle the error in the given response with the given resolved status code.
* <p>This default implementation throws a {@link HttpClientErrorException} if the response status code
* is {@link org.springframework.http.HttpStatus.Series#CLIENT_ERROR}, a {@link HttpServerErrorException}
* if it is {@link org.springframework.http.HttpStatus.Series#SERVER_ERROR},
* and a {@link RestClientException} in other cases.
* @since 5.0
*/
protected void handleError(@Nullable URI url, @Nullable HttpMethod method, ClientHttpResponse response,
HttpStatus statusCode) throws IOException {

String statusText = response.getStatusText();
HttpHeaders headers = response.getHeaders();
byte[] body = getResponseBody(response);
Charset charset = getCharset(response);
String message = httpErrorDetailsExtractor.getErrorDetails(statusCode.value(), statusText, body, charset, url, method);

switch (statusCode.series()) {
case CLIENT_ERROR:
throw HttpClientErrorException.create(statusCode, statusText, headers, body, charset);
throw HttpClientErrorException.create(message, statusCode, statusText, headers, body, charset, url, method);
case SERVER_ERROR:
throw HttpServerErrorException.create(statusCode, statusText, headers, body, charset);
throw HttpServerErrorException.create(message, statusCode, statusText, headers, body, charset, url, method);
default:
throw new UnknownHttpStatusCodeException(statusCode.value(), statusText, headers, body, charset);
throw new UnknownHttpStatusCodeException(message, statusCode.value(), statusText, headers, body,
charset, url, method);
}
}

Expand Down