Skip to content

Commit

Permalink
SPR-17130 http error details in the exception message
Browse files Browse the repository at this point in the history
  • Loading branch information
jerzykrlk committed Dec 29, 2018
1 parent 78a6429 commit fcda0ad
Show file tree
Hide file tree
Showing 10 changed files with 511 additions and 65 deletions.
@@ -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 @@ -113,17 +139,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

0 comments on commit fcda0ad

Please sign in to comment.