From f0296d843d625129c8ebbb4208d2ea9a87c0cc3d Mon Sep 17 00:00:00 2001 From: jerzykrlk Date: Mon, 20 Aug 2018 21:15:02 +0200 Subject: [PATCH] SPR-17130 http error details in the exception message --- .../DefaultHttpErrorDetailsExtractor.java | 138 ++++++++++++++++++ .../client/DefaultResponseErrorHandler.java | 54 ++++++- .../web/client/HttpClientErrorException.java | 104 ++++++++----- .../web/client/HttpErrorDetailsExtractor.java | 33 +++++ .../web/client/HttpServerErrorException.java | 55 ++++--- .../web/client/HttpStatusCodeException.java | 26 +++- .../client/RestClientResponseException.java | 40 +++++ .../UnknownHttpStatusCodeException.java | 22 ++- ...DefaultHttpErrorDetailsExtractorTests.java | 97 ++++++++++++ .../client/RestTemplateIntegrationTests.java | 2 +- 10 files changed, 507 insertions(+), 64 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/client/DefaultHttpErrorDetailsExtractor.java create mode 100644 spring-web/src/main/java/org/springframework/web/client/HttpErrorDetailsExtractor.java create mode 100644 spring-web/src/test/java/org/springframework/web/client/DefaultHttpErrorDetailsExtractorTests.java diff --git a/spring-web/src/main/java/org/springframework/web/client/DefaultHttpErrorDetailsExtractor.java b/spring-web/src/main/java/org/springframework/web/client/DefaultHttpErrorDetailsExtractor.java new file mode 100644 index 000000000000..a1aefcabd315 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/client/DefaultHttpErrorDetailsExtractor.java @@ -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. + * + *

This extractor will compose a short summary of the http error response, including: + *

+ * + * An example: + *
+ * 404 Not Found after GET http://example.com:8080/my-endpoint : [{'id': 123, 'message': 'my very long... (500 bytes)]
+ * 
+ * + * @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:
404 Not Found after GET http://example.com:8080/my-endpoint : [{'id': 123, 'message': 'my very long... (500 bytes)]
+ */ + @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; + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java b/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java index 536e7339673c..7a422076f06e 100644 --- a/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java +++ b/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java @@ -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; /** @@ -43,6 +46,17 @@ */ public class DefaultResponseErrorHandler implements ResponseErrorHandler { + private HttpErrorDetailsExtractor httpErrorDetailsExtractor = new DefaultHttpErrorDetailsExtractor(); + + /** + * Set the error summary extractor. + *

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)} with the response status code. */ @@ -67,16 +81,25 @@ protected boolean hasError(HttpStatus statusCode) { } /** - * Delegates to {@link #handleError(ClientHttpResponse, HttpStatus)} with the response status code. + * Delegates to {@link #handleError(URI, HttpMethod, ClientHttpResponse)} with null URI and method. */ @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. + */ + @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); } /** @@ -88,17 +111,34 @@ public void handleError(ClientHttpResponse response) throws IOException { * @since 5.0 */ 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. + *

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); } } diff --git a/spring-web/src/main/java/org/springframework/web/client/HttpClientErrorException.java b/spring-web/src/main/java/org/springframework/web/client/HttpClientErrorException.java index 0e271a96b503..b82e1dc69ac3 100644 --- a/spring-web/src/main/java/org/springframework/web/client/HttpClientErrorException.java +++ b/spring-web/src/main/java/org/springframework/web/client/HttpClientErrorException.java @@ -16,9 +16,11 @@ package org.springframework.web.client; +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.lang.Nullable; @@ -66,39 +68,49 @@ public HttpClientErrorException(HttpStatus statusCode, String statusText, super(statusCode, statusText, headers, body, responseCharset); } + /** + * Constructor with a status code and status text, headers, content, request URL and method. + */ + public HttpClientErrorException(String message, HttpStatus statusCode, String statusText, + @Nullable HttpHeaders headers, @Nullable byte[] body, @Nullable Charset responseCharset, + @Nullable URI url, @Nullable HttpMethod method) { + + super(message, statusCode, statusText, headers, body, responseCharset, url, method); + } /** * Create {@code HttpClientErrorException} or an HTTP status specific sub-class. * @since 5.1 */ public static HttpClientErrorException create( - HttpStatus statusCode, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { + String message, HttpStatus statusCode, String statusText, HttpHeaders headers, byte[] body, + @Nullable Charset charset, @Nullable URI url, @Nullable HttpMethod method) { switch (statusCode) { case BAD_REQUEST: - return new HttpClientErrorException.BadRequest(statusText, headers, body, charset); + return new HttpClientErrorException.BadRequest(message, statusText, headers, body, charset, url, method); case UNAUTHORIZED: - return new HttpClientErrorException.Unauthorized(statusText, headers, body, charset); + return new HttpClientErrorException.Unauthorized(message, statusText, headers, body, charset, url, method); case FORBIDDEN: - return new HttpClientErrorException.Forbidden(statusText, headers, body, charset); + return new HttpClientErrorException.Forbidden(message, statusText, headers, body, charset, url, method); case NOT_FOUND: - return new HttpClientErrorException.NotFound(statusText, headers, body, charset); + return new HttpClientErrorException.NotFound(message, statusText, headers, body, charset, url, method); case METHOD_NOT_ALLOWED: - return new HttpClientErrorException.MethodNotAllowed(statusText, headers, body, charset); + return new HttpClientErrorException.MethodNotAllowed(message, statusText, headers, body, charset, url, method); case NOT_ACCEPTABLE: - return new HttpClientErrorException.NotAcceptable(statusText, headers, body, charset); + return new HttpClientErrorException.NotAcceptable(message, statusText, headers, body, charset, url, method); case CONFLICT: - return new HttpClientErrorException.Conflict(statusText, headers, body, charset); + return new HttpClientErrorException.Conflict(message, statusText, headers, body, charset, url, method); case GONE: - return new HttpClientErrorException.Gone(statusText, headers, body, charset); + return new HttpClientErrorException.Gone(message, statusText, headers, body, charset, url, method); case UNSUPPORTED_MEDIA_TYPE: - return new HttpClientErrorException.UnsupportedMediaType(statusText, headers, body, charset); + return new HttpClientErrorException.UnsupportedMediaType(message, statusText, headers, body, charset, url, method); case TOO_MANY_REQUESTS: - return new HttpClientErrorException.TooManyRequests(statusText, headers, body, charset); + return new HttpClientErrorException.TooManyRequests(message, statusText, headers, body, charset, url, method); case UNPROCESSABLE_ENTITY: - return new HttpClientErrorException.UnprocessableEntity(statusText, headers, body, charset); + return new HttpClientErrorException.UnprocessableEntity(message, statusText, headers, body, charset, url, method); default: - return new HttpClientErrorException(statusCode, statusText, headers, body, charset); + return new HttpClientErrorException(message, statusCode, statusText, headers, body, charset, url, method); } } @@ -112,8 +124,10 @@ public static HttpClientErrorException create( @SuppressWarnings("serial") public static class BadRequest extends HttpClientErrorException { - BadRequest(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(HttpStatus.BAD_REQUEST, statusText, headers, body, charset); + BadRequest(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, + @Nullable URI url, @Nullable HttpMethod method) { + + super(message, HttpStatus.BAD_REQUEST, statusText, headers, body, charset, url, method); } } @@ -124,8 +138,10 @@ public static class BadRequest extends HttpClientErrorException { @SuppressWarnings("serial") public static class Unauthorized extends HttpClientErrorException { - Unauthorized(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(HttpStatus.UNAUTHORIZED, statusText, headers, body, charset); + Unauthorized(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, + @Nullable URI url, @Nullable HttpMethod method) { + + super(message, HttpStatus.UNAUTHORIZED, statusText, headers, body, charset, url, method); } } @@ -136,8 +152,10 @@ public static class Unauthorized extends HttpClientErrorException { @SuppressWarnings("serial") public static class Forbidden extends HttpClientErrorException { - Forbidden(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(HttpStatus.FORBIDDEN, statusText, headers, body, charset); + Forbidden(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, + @Nullable URI url, @Nullable HttpMethod method) { + + super(message, HttpStatus.FORBIDDEN, statusText, headers, body, charset, url, method); } } @@ -148,8 +166,10 @@ public static class Forbidden extends HttpClientErrorException { @SuppressWarnings("serial") public static class NotFound extends HttpClientErrorException { - NotFound(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(HttpStatus.NOT_FOUND, statusText, headers, body, charset); + NotFound(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, + @Nullable URI url, @Nullable HttpMethod method) { + + super(message, HttpStatus.NOT_FOUND, statusText, headers, body, charset, url, method); } } @@ -160,8 +180,10 @@ public static class NotFound extends HttpClientErrorException { @SuppressWarnings("serial") public static class MethodNotAllowed extends HttpClientErrorException { - MethodNotAllowed(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(HttpStatus.METHOD_NOT_ALLOWED, statusText, headers, body, charset); + MethodNotAllowed(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, + @Nullable URI url, @Nullable HttpMethod method) { + + super(message, HttpStatus.METHOD_NOT_ALLOWED, statusText, headers, body, charset, url, method); } } @@ -172,8 +194,10 @@ public static class MethodNotAllowed extends HttpClientErrorException { @SuppressWarnings("serial") public static class NotAcceptable extends HttpClientErrorException { - NotAcceptable(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(HttpStatus.NOT_ACCEPTABLE, statusText, headers, body, charset); + NotAcceptable(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, + @Nullable URI url, @Nullable HttpMethod method) { + + super(message, HttpStatus.NOT_ACCEPTABLE, statusText, headers, body, charset, url, method); } } @@ -184,8 +208,10 @@ public static class NotAcceptable extends HttpClientErrorException { @SuppressWarnings("serial") public static class Conflict extends HttpClientErrorException { - Conflict(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(HttpStatus.CONFLICT, statusText, headers, body, charset); + Conflict(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, + @Nullable URI url, @Nullable HttpMethod method) { + + super(message, HttpStatus.CONFLICT, statusText, headers, body, charset, url, method); } } @@ -196,8 +222,10 @@ public static class Conflict extends HttpClientErrorException { @SuppressWarnings("serial") public static class Gone extends HttpClientErrorException { - Gone(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(HttpStatus.GONE, statusText, headers, body, charset); + Gone(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, + @Nullable URI url, @Nullable HttpMethod method) { + + super(message, HttpStatus.GONE, statusText, headers, body, charset, url, method); } } @@ -208,8 +236,10 @@ public static class Gone extends HttpClientErrorException { @SuppressWarnings("serial") public static class UnsupportedMediaType extends HttpClientErrorException { - UnsupportedMediaType(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(HttpStatus.UNSUPPORTED_MEDIA_TYPE, statusText, headers, body, charset); + UnsupportedMediaType(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, + @Nullable URI url, @Nullable HttpMethod method) { + + super(message, HttpStatus.UNSUPPORTED_MEDIA_TYPE, statusText, headers, body, charset, url, method); } } @@ -220,8 +250,10 @@ public static class UnsupportedMediaType extends HttpClientErrorException { @SuppressWarnings("serial") public static class UnprocessableEntity extends HttpClientErrorException { - UnprocessableEntity(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(HttpStatus.UNPROCESSABLE_ENTITY, statusText, headers, body, charset); + UnprocessableEntity(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, + @Nullable URI url, @Nullable HttpMethod method) { + + super(message, HttpStatus.UNPROCESSABLE_ENTITY, statusText, headers, body, charset, url, method); } } @@ -232,8 +264,10 @@ public static class UnprocessableEntity extends HttpClientErrorException { @SuppressWarnings("serial") public static class TooManyRequests extends HttpClientErrorException { - TooManyRequests(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(HttpStatus.TOO_MANY_REQUESTS, statusText, headers, body, charset); + TooManyRequests(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, + @Nullable URI url, @Nullable HttpMethod method) { + + super(message, HttpStatus.TOO_MANY_REQUESTS, statusText, headers, body, charset, url, method); } } diff --git a/spring-web/src/main/java/org/springframework/web/client/HttpErrorDetailsExtractor.java b/spring-web/src/main/java/org/springframework/web/client/HttpErrorDetailsExtractor.java new file mode 100644 index 000000000000..d141549c8569 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/client/HttpErrorDetailsExtractor.java @@ -0,0 +1,33 @@ +package org.springframework.web.client; + +import java.net.URI; +import java.nio.charset.Charset; + +import org.jetbrains.annotations.NotNull; +import org.springframework.http.HttpMethod; +import org.springframework.lang.Nullable; + +/** + * Strategy interface used by the {@link DefaultResponseErrorHandler} to compose + * a summary of the http error. + * + * @author Jerzy Krolak + * @since 5.1 + */ +public interface HttpErrorDetailsExtractor { + + /** + * Assemble HTTP error response details string, based on the provided response details. + * @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 + */ + @NotNull + String getErrorDetails(int rawStatusCode, String statusText, @Nullable byte[] responseBody, + @Nullable Charset responseCharset, @Nullable URI url, @Nullable HttpMethod method); + +} diff --git a/spring-web/src/main/java/org/springframework/web/client/HttpServerErrorException.java b/spring-web/src/main/java/org/springframework/web/client/HttpServerErrorException.java index 2173faa839dd..dbb8bdf79d87 100644 --- a/spring-web/src/main/java/org/springframework/web/client/HttpServerErrorException.java +++ b/spring-web/src/main/java/org/springframework/web/client/HttpServerErrorException.java @@ -16,9 +16,11 @@ package org.springframework.web.client; +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.lang.Nullable; @@ -66,27 +68,36 @@ public HttpServerErrorException(HttpStatus statusCode, String statusText, super(statusCode, statusText, headers, body, charset); } + /** + * Constructor with a status code and status text, headers, content, request URL, and method. + */ + public HttpServerErrorException(String message, HttpStatus statusCode, String statusText, + @Nullable HttpHeaders headers, @Nullable byte[] body, @Nullable Charset charset, + @Nullable URI url, @Nullable HttpMethod method) { + super(message, statusCode, statusText, headers, body, charset, url, method); + } /** * Create an {@code HttpServerErrorException} or an HTTP status specific sub-class. * @since 5.1 */ public static HttpServerErrorException create( - HttpStatus statusCode, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { + String message, HttpStatus statusCode, String statusText, HttpHeaders headers, byte[] body, + @Nullable Charset charset, @Nullable URI url, @Nullable HttpMethod method) { switch (statusCode) { case INTERNAL_SERVER_ERROR: - return new HttpServerErrorException.InternalServerError(statusText, headers, body, charset); + return new HttpServerErrorException.InternalServerError(message, statusText, headers, body, charset, url, method); case NOT_IMPLEMENTED: - return new HttpServerErrorException.NotImplemented(statusText, headers, body, charset); + return new HttpServerErrorException.NotImplemented(message, statusText, headers, body, charset, url, method); case BAD_GATEWAY: - return new HttpServerErrorException.BadGateway(statusText, headers, body, charset); + return new HttpServerErrorException.BadGateway(message, statusText, headers, body, charset, url, method); case SERVICE_UNAVAILABLE: - return new HttpServerErrorException.ServiceUnavailable(statusText, headers, body, charset); + return new HttpServerErrorException.ServiceUnavailable(message, statusText, headers, body, charset, url, method); case GATEWAY_TIMEOUT: - return new HttpServerErrorException.GatewayTimeout(statusText, headers, body, charset); + return new HttpServerErrorException.GatewayTimeout(message, statusText, headers, body, charset, url, method); default: - return new HttpServerErrorException(statusCode, statusText, headers, body, charset); + return new HttpServerErrorException(message, statusCode, statusText, headers, body, charset, url, method); } } @@ -100,8 +111,10 @@ public static HttpServerErrorException create( @SuppressWarnings("serial") public static class InternalServerError extends HttpServerErrorException { - InternalServerError(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(HttpStatus.INTERNAL_SERVER_ERROR, statusText, headers, body, charset); + InternalServerError(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, + @Nullable URI url, @Nullable HttpMethod method) { + + super(message, HttpStatus.INTERNAL_SERVER_ERROR, statusText, headers, body, charset, url, method); } } @@ -112,8 +125,10 @@ public static class InternalServerError extends HttpServerErrorException { @SuppressWarnings("serial") public static class NotImplemented extends HttpServerErrorException { - NotImplemented(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(HttpStatus.NOT_IMPLEMENTED, statusText, headers, body, charset); + NotImplemented(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, + @Nullable URI url, @Nullable HttpMethod method) { + + super(message, HttpStatus.NOT_IMPLEMENTED, statusText, headers, body, charset, url, method); } } @@ -124,8 +139,10 @@ public static class NotImplemented extends HttpServerErrorException { @SuppressWarnings("serial") public static class BadGateway extends HttpServerErrorException { - BadGateway(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(HttpStatus.BAD_GATEWAY, statusText, headers, body, charset); + BadGateway(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, + @Nullable URI url, @Nullable HttpMethod method) { + + super(message, HttpStatus.BAD_GATEWAY, statusText, headers, body, charset, url, method); } } @@ -136,8 +153,10 @@ public static class BadGateway extends HttpServerErrorException { @SuppressWarnings("serial") public static class ServiceUnavailable extends HttpServerErrorException { - ServiceUnavailable(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(HttpStatus.SERVICE_UNAVAILABLE, statusText, headers, body, charset); + ServiceUnavailable(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, + @Nullable URI url, @Nullable HttpMethod method) { + + super(message, HttpStatus.SERVICE_UNAVAILABLE, statusText, headers, body, charset, url, method); } } @@ -148,8 +167,10 @@ public static class ServiceUnavailable extends HttpServerErrorException { @SuppressWarnings("serial") public static class GatewayTimeout extends HttpServerErrorException { - GatewayTimeout(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(HttpStatus.GATEWAY_TIMEOUT, statusText, headers, body, charset); + GatewayTimeout(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, + @Nullable URI url, @Nullable HttpMethod method) { + + super(message, HttpStatus.GATEWAY_TIMEOUT, statusText, headers, body, charset, url, method); } } diff --git a/spring-web/src/main/java/org/springframework/web/client/HttpStatusCodeException.java b/spring-web/src/main/java/org/springframework/web/client/HttpStatusCodeException.java index a4f22478edbd..5c41605f76d0 100644 --- a/spring-web/src/main/java/org/springframework/web/client/HttpStatusCodeException.java +++ b/spring-web/src/main/java/org/springframework/web/client/HttpStatusCodeException.java @@ -16,9 +16,11 @@ package org.springframework.web.client; +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.lang.Nullable; @@ -82,8 +84,28 @@ protected HttpStatusCodeException(HttpStatus statusCode, String statusText, protected HttpStatusCodeException(HttpStatus statusCode, String statusText, @Nullable HttpHeaders responseHeaders, @Nullable byte[] responseBody, @Nullable Charset responseCharset) { - super(statusCode.value() + " " + statusText, statusCode.value(), statusText, - responseHeaders, responseBody, responseCharset); + this(statusCode.value() + " " + statusText, statusCode, statusText, + responseHeaders, responseBody, responseCharset, null, null); + } + + /** + * Construct instance with an {@link HttpStatus}, status text, content, and + * a response charset. + * @param message the exception message + * @param statusCode the status code + * @param statusText the status text + * @param responseHeaders the response headers, may be {@code null} + * @param responseBody the response body content, may be {@code null} + * @param responseCharset the response body charset, may be {@code null} + * @param url the request URL, may be {@code null} + * @param method the request method, may be {@code null} + */ + protected HttpStatusCodeException(String message, HttpStatus statusCode, String statusText, + @Nullable HttpHeaders responseHeaders, @Nullable byte[] responseBody, @Nullable Charset responseCharset, + @Nullable URI url, @Nullable HttpMethod method) { + + super(message, statusCode.value(), statusText, responseHeaders, responseBody, + responseCharset, url, method); this.statusCode = statusCode; } diff --git a/spring-web/src/main/java/org/springframework/web/client/RestClientResponseException.java b/spring-web/src/main/java/org/springframework/web/client/RestClientResponseException.java index 079eb7dadc54..80a263de8739 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestClientResponseException.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestClientResponseException.java @@ -17,10 +17,12 @@ package org.springframework.web.client; import java.io.UnsupportedEncodingException; +import java.net.URI; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.lang.Nullable; /** @@ -48,6 +50,12 @@ public class RestClientResponseException extends RestClientException { @Nullable private final String responseCharset; + @Nullable + private final HttpMethod method; + + @Nullable + private final URI url; + /** * Construct a new instance of with the given response data. @@ -59,6 +67,22 @@ public class RestClientResponseException extends RestClientException { */ public RestClientResponseException(String message, int statusCode, String statusText, @Nullable HttpHeaders responseHeaders, @Nullable byte[] responseBody, @Nullable Charset responseCharset) { + this(message, statusCode, statusText, responseHeaders, responseBody, responseCharset, null, null); + } + + /** + * Construct a new instance of with the given response data. + * @param statusCode the raw status code value + * @param statusText the status text + * @param responseHeaders the response headers (may be {@code null}) + * @param responseBody the response body content (may be {@code null}) + * @param responseCharset the response body charset (may be {@code null}) + * @param url the request URL (may be {@code null}) + * @param method the request method (may be {@code null}) + */ + public RestClientResponseException(String message, int statusCode, String statusText, + @Nullable HttpHeaders responseHeaders, @Nullable byte[] responseBody, @Nullable Charset responseCharset, + @Nullable URI url, @Nullable HttpMethod method) { super(message); this.rawStatusCode = statusCode; @@ -66,6 +90,8 @@ public RestClientResponseException(String message, int statusCode, String status this.responseHeaders = responseHeaders; this.responseBody = (responseBody != null ? responseBody : new byte[0]); this.responseCharset = (responseCharset != null ? responseCharset.name() : null); + this.method = method; + this.url = url; } @@ -98,6 +124,20 @@ public byte[] getResponseBodyAsByteArray() { return this.responseBody; } + /** + * Return the HTTP request method. + */ + public HttpMethod getMethod() { + return method; + } + + /** + * Return the HTTP request url. + */ + public URI getUrl() { + return url; + } + /** * Return the response body as a string. */ diff --git a/spring-web/src/main/java/org/springframework/web/client/UnknownHttpStatusCodeException.java b/spring-web/src/main/java/org/springframework/web/client/UnknownHttpStatusCodeException.java index 593fd665fa70..ffb9b078d8ce 100644 --- a/spring-web/src/main/java/org/springframework/web/client/UnknownHttpStatusCodeException.java +++ b/spring-web/src/main/java/org/springframework/web/client/UnknownHttpStatusCodeException.java @@ -16,9 +16,11 @@ package org.springframework.web.client; +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.lang.Nullable; @@ -45,8 +47,24 @@ public class UnknownHttpStatusCodeException extends RestClientResponseException public UnknownHttpStatusCodeException(int rawStatusCode, String statusText, @Nullable HttpHeaders responseHeaders, @Nullable byte[] responseBody, @Nullable Charset responseCharset) { - super("Unknown status code [" + rawStatusCode + "]" + " " + statusText, - rawStatusCode, statusText, responseHeaders, responseBody, responseCharset); + this("Unknown status code [" + rawStatusCode + "]" + " " + statusText, + rawStatusCode, statusText, responseHeaders, responseBody, responseCharset, null, null); } + /** + * Construct a new instance of {@code HttpStatusCodeException} based on an + * {@link HttpStatus}, status text, and response body content. + * @param rawStatusCode the raw status code value + * @param statusText the status text + * @param responseHeaders the response headers (may be {@code null}) + * @param responseBody the response body content (may be {@code null}) + * @param responseCharset the response body charset (may be {@code null}) + * @param url the request URI (may be {@code null}) + * @param method the request HTTP method (may be {@code null}) + */ + public UnknownHttpStatusCodeException(String message, int rawStatusCode, String statusText, @Nullable HttpHeaders responseHeaders, + @Nullable byte[] responseBody, @Nullable Charset responseCharset, @Nullable URI url, @Nullable HttpMethod method) { + + super(message, rawStatusCode, statusText, responseHeaders, responseBody, responseCharset, url, method); + } } diff --git a/spring-web/src/test/java/org/springframework/web/client/DefaultHttpErrorDetailsExtractorTests.java b/spring-web/src/test/java/org/springframework/web/client/DefaultHttpErrorDetailsExtractorTests.java new file mode 100644 index 000000000000..94804ac69f8a --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/client/DefaultHttpErrorDetailsExtractorTests.java @@ -0,0 +1,97 @@ +package org.springframework.web.client; + +import java.net.URI; + +import com.google.common.base.Strings; +import org.junit.Test; + +import static java.nio.charset.StandardCharsets.*; +import static org.junit.Assert.*; +import static org.springframework.http.HttpMethod.*; +import static org.springframework.http.HttpStatus.*; + +public class DefaultHttpErrorDetailsExtractorTests { + + private final DefaultHttpErrorDetailsExtractor extractor = new DefaultHttpErrorDetailsExtractor(); + + @Test + public void shouldGetSimpleExceptionMessage() { + String actual = extractor.getErrorDetails(NOT_FOUND.value(), "Not Found", null, null, null, null); + + assertEquals("Should get a simple message", "404 Not Found", actual); + } + + @Test + public void shouldGetCompleteMessageWithoutBody() { + String actual = extractor.getErrorDetails(NOT_FOUND.value(), "Not Found", null, null, URI.create("http://localhost:8080/my-endpoint"), GET); + + assertEquals("Should get a complete message without body", "404 Not Found after GET http://localhost:8080/my-endpoint : [no body]", actual); + } + + @Test + public void shouldGetCompleteMessageWithShortAsciiBodyNoCharset() { + String responseBody = "my short response body"; + + String actual = extractor.getErrorDetails(NOT_FOUND.value(), "Not Found", responseBody.getBytes(), null, URI.create("http://localhost:8080/my-endpoint"), GET); + + assertEquals("Should get a simple message", "404 Not Found after GET http://localhost:8080/my-endpoint : [my short response body]", actual); + } + + @Test + public void shouldGetCompleteMessageWithShortAsciiBodyUtfCharset() { + String responseBody = "my short response body"; + + String actual = extractor.getErrorDetails(NOT_FOUND.value(), "Not Found", responseBody.getBytes(), UTF_8, URI.create("http://localhost:8080/my-endpoint"), GET); + + assertEquals("Should get a simple message", "404 Not Found after GET http://localhost:8080/my-endpoint : [my short response body]", actual); + } + + @Test + public void shouldGetCompleteMessageWithShortUtfBodyUtfCharset() { + String responseBody = "my short response body \u0105\u0119"; + + String actual = extractor.getErrorDetails(NOT_FOUND.value(), "Not Found", responseBody.getBytes(), UTF_8, URI.create("http://localhost:8080/my-endpoint"), GET); + + assertEquals("Should get a simple message", "404 Not Found after GET http://localhost:8080/my-endpoint : [my short response body \u0105\u0119]", actual); + } + + @Test + public void shouldGetCompleteMessageWithShortUtfBodyNoCharset() { + String responseBody = "my short response body \u0105\u0119"; + + String actual = extractor.getErrorDetails(NOT_FOUND.value(), "Not Found", responseBody.getBytes(UTF_8), null, URI.create("http://localhost:8080/my-endpoint"), GET); + + assertEquals("Should get a simple message", "404 Not Found after GET http://localhost:8080/my-endpoint : [my short response body \u00c4\u0085\u00c4\u0099]", actual); + } + + @Test + public void shouldGetCompleteMessageWithLongAsciiBodyNoCharset() { + String responseBody = Strings.repeat("asdfg", 100); + String expectedMessage = "404 Not Found after GET http://localhost:8080/my-endpoint : [" + Strings.repeat("asdfg", 40) + "... (500 bytes)]"; + + String actual = extractor.getErrorDetails(NOT_FOUND.value(), "Not Found", responseBody.getBytes(UTF_8), null, URI.create("http://localhost:8080/my-endpoint"), GET); + + assertEquals("Should get a simple message", expectedMessage, actual); + } + + @Test + public void shouldGetCompleteMessageWithLongUtfBodyNoCharset() { + String responseBody = Strings.repeat("asd\u0105\u0119", 100); + String expectedMessage = "404 Not Found after GET http://localhost:8080/my-endpoint : [" + Strings.repeat("asd\u00c4\u0085\u00c4\u0099", 28) + "asd\u00c4... (700 bytes)]"; + + String actual = extractor.getErrorDetails(NOT_FOUND.value(), "Not Found", responseBody.getBytes(UTF_8), null, URI.create("http://localhost:8080/my-endpoint"), GET); + + assertEquals("Should get a simple message", expectedMessage, actual); + } + + @Test + public void shouldGetCompleteMessageWithLongUtfBodyUtfCharset() { + String responseBody = Strings.repeat("asd\u0105\u0119", 100); + String expectedMessage = "404 Not Found after GET http://localhost:8080/my-endpoint : [" + Strings.repeat("asd\u0105\u0119", 40) + "... (700 bytes)]"; + + String actual = extractor.getErrorDetails(NOT_FOUND.value(), "Not Found", responseBody.getBytes(UTF_8), UTF_8, URI.create("http://localhost:8080/my-endpoint"), GET); + + assertEquals("Should get a simple message", expectedMessage, actual); + } + +} \ No newline at end of file diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java index e81e10f9babd..1bf6dd00ff10 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java @@ -188,7 +188,7 @@ public void badRequest() { } catch (HttpClientErrorException.BadRequest ex) { assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode()); - assertEquals("400 Client Error", ex.getMessage()); + assertEquals("400 Client Error after GET http://localhost:" + port + "/status/badrequest : [no body]", ex.getMessage()); } }