From 91ec274b10aedd54eb63e3c67e0978c56cc81e28 Mon Sep 17 00:00:00 2001 From: jerzykrlk Date: Mon, 20 Aug 2018 21:15:02 +0200 Subject: [PATCH 1/2] SPR-17130 http error details in the exception message --- .../DefaultHttpErrorDetailsExtractor.java | 138 ++++++++++++++++++ .../client/DefaultResponseErrorHandler.java | 59 +++++++- .../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 | 3 +- 10 files changed, 512 insertions(+), 65 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 520965640c74..8a9ebdeb1d59 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)} (for a standard status enum value) or * {@link #hasError(int)} (for an unknown status code) with the response status code. @@ -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); } /** @@ -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. + *

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 a8f3328af2a8..70c92265a3c6 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 b0f7cc85f3f5..4a63be5cf160 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 16715ef81151..726e77558737 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; import org.springframework.util.StringUtils; @@ -83,8 +85,28 @@ protected HttpStatusCodeException(HttpStatus statusCode, String statusText, protected HttpStatusCodeException(HttpStatus statusCode, String statusText, @Nullable HttpHeaders responseHeaders, @Nullable byte[] responseBody, @Nullable Charset responseCharset) { - super(getMessage(statusCode, statusText), statusCode.value(), statusText, - responseHeaders, responseBody, responseCharset); + this(getMessage(statusCode, 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 2a927f376905..5410096477a4 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 converted to String. The charset used is that * of the response "Content-Type" or otherwise {@code "UTF-8"}. 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 ac239513ae90..79d5544ad022 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 d74c4c767765..631725b6bd29 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 @@ -254,7 +254,8 @@ void badRequest(ClientHttpRequestFactory clientHttpRequestFactory) { template.execute(baseUrl + "/status/badrequest", HttpMethod.GET, null, null)) .satisfies(ex -> { assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); - assertThat(ex.getMessage()).isEqualTo("400 Client Error"); + assertThat(ex.getMessage()).isEqualTo( + "400 Client Error after GET http://localhost:" + port + "/status/badrequest : [no body]"); }); } From 04aa3d05dace092a675609425e9ee9274d5882be Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 13 Nov 2019 14:18:30 +0000 Subject: [PATCH 2/2] Refactor solution for respones error details See gh-1956 --- .../DefaultHttpErrorDetailsExtractor.java | 138 ------------ .../client/DefaultResponseErrorHandler.java | 100 ++++----- .../web/client/HttpClientErrorException.java | 204 ++++++++++++------ .../web/client/HttpErrorDetailsExtractor.java | 33 --- .../web/client/HttpServerErrorException.java | 114 ++++++---- .../web/client/HttpStatusCodeException.java | 15 +- .../client/RestClientResponseException.java | 40 ---- .../UnknownHttpStatusCodeException.java | 15 +- ...DefaultHttpErrorDetailsExtractorTests.java | 97 --------- .../DefaultResponseErrorHandlerTests.java | 30 ++- .../client/RestTemplateIntegrationTests.java | 3 +- 11 files changed, 306 insertions(+), 483 deletions(-) delete mode 100644 spring-web/src/main/java/org/springframework/web/client/DefaultHttpErrorDetailsExtractor.java delete mode 100644 spring-web/src/main/java/org/springframework/web/client/HttpErrorDetailsExtractor.java delete 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 deleted file mode 100644 index a1aefcabd315..000000000000 --- a/spring-web/src/main/java/org/springframework/web/client/DefaultHttpErrorDetailsExtractor.java +++ /dev/null @@ -1,138 +0,0 @@ -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 8a9ebdeb1d59..ff72eb1e465d 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 @@ -16,18 +16,21 @@ package org.springframework.web.client; +import java.io.ByteArrayInputStream; import java.io.IOException; -import java.net.URI; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.CharBuffer; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; 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; +import org.springframework.util.ObjectUtils; /** * Spring's default implementation of the {@link ResponseErrorHandler} interface. @@ -46,17 +49,6 @@ */ 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)} (for a standard status enum value) or * {@link #hasError(int)} (for an unknown status code) with the response status code. @@ -101,31 +93,58 @@ protected boolean hasError(int unknownStatusCode) { } /** - * Delegates to {@link #handleError(URI, HttpMethod, ClientHttpResponse, HttpStatus)} with the + * Delegates to {@link #handleError(ClientHttpResponse, HttpStatus)} with the * response status code. * @throws UnknownHttpStatusCodeException in case of an unresolvable status code - * @see #handleError(URI, HttpMethod, ClientHttpResponse, HttpStatus) + * @see #handleError(ClientHttpResponse, HttpStatus) */ @Override public void handleError(ClientHttpResponse response) throws IOException { - handleError(null, null, response); + HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode()); + if (statusCode == null) { + String message = getErrorMessage( + response.getRawStatusCode(), response.getStatusText(), + getResponseBody(response), getCharset(response)); + throw new UnknownHttpStatusCodeException(message, + response.getRawStatusCode(), response.getStatusText(), + response.getHeaders(), getResponseBody(response), getCharset(response)); + } + handleError(response, statusCode); } /** - * 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) + * Return error message with details from the response body, possibly truncated: + *

+	 * 404 Not Found: [{'id': 123, 'message': 'my very long... (500 bytes)]
+	 * 
*/ - @Override - public void handleError(URI url, HttpMethod method, ClientHttpResponse response) throws IOException { - HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode()); - if (statusCode == null) { - 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); + private String getErrorMessage( + int rawStatusCode, String statusText, @Nullable byte[] responseBody, @Nullable Charset charset) { + + String preface = rawStatusCode + " " + statusText + ": "; + if (ObjectUtils.isEmpty(responseBody)) { + return preface + "[no body]"; + } + + charset = charset == null ? StandardCharsets.UTF_8 : charset; + int maxChars = 200; + + if (responseBody.length < maxChars * 2) { + return preface + "[" + new String(responseBody, charset) + "]"; + } + + try { + Reader reader = new InputStreamReader(new ByteArrayInputStream(responseBody), charset); + CharBuffer buffer = CharBuffer.allocate(maxChars); + reader.read(buffer); + reader.close(); + buffer.flip(); + return preface + "[" + buffer.toString() + "... (" + responseBody.length + " bytes)]"; + } + catch (IOException ex) { + // should never happen + throw new IllegalStateException(ex); } - handleError(url, method, response, statusCode); } /** @@ -140,34 +159,19 @@ public void handleError(URI url, HttpMethod method, ClientHttpResponse response) * @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. - *

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); + String message = getErrorMessage(statusCode.value(), statusText, body, charset); switch (statusCode.series()) { case CLIENT_ERROR: - throw HttpClientErrorException.create(message, statusCode, statusText, headers, body, charset, url, method); + throw HttpClientErrorException.create(message, statusCode, statusText, headers, body, charset); case SERVER_ERROR: - throw HttpServerErrorException.create(message, statusCode, statusText, headers, body, charset, url, method); + throw HttpServerErrorException.create(message, statusCode, statusText, headers, body, charset); default: - throw new UnknownHttpStatusCodeException(message, statusCode.value(), statusText, headers, body, - charset, url, method); + throw new UnknownHttpStatusCodeException(message, statusCode.value(), statusText, headers, body, charset); } } 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 70c92265a3c6..ff6e3f83085a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,9 @@ 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; @@ -69,48 +67,84 @@ public HttpClientErrorException(HttpStatus statusCode, String statusText, } /** - * Constructor with a status code and status text, headers, content, request URL and method. + * Constructor with a status code and status text, headers, and content, + * and an prepared message. + * @since 5.2.2 */ public HttpClientErrorException(String message, HttpStatus statusCode, String statusText, - @Nullable HttpHeaders headers, @Nullable byte[] body, @Nullable Charset responseCharset, - @Nullable URI url, @Nullable HttpMethod method) { + @Nullable HttpHeaders headers, @Nullable byte[] body, @Nullable Charset responseCharset) { - super(message, statusCode, statusText, headers, body, responseCharset, url, method); + super(message, statusCode, statusText, headers, body, responseCharset); } + /** * Create {@code HttpClientErrorException} or an HTTP status specific sub-class. * @since 5.1 */ public static HttpClientErrorException create( - String message, HttpStatus statusCode, String statusText, HttpHeaders headers, byte[] body, - @Nullable Charset charset, @Nullable URI url, @Nullable HttpMethod method) { + HttpStatus statusCode, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { + + return create(null, statusCode, statusText, headers, body, charset); + } + + /** + * Variant of {@link #create(HttpStatus, String, HttpHeaders, byte[], Charset)} + * with an optional prepared message. + * @since 5.2.2 + */ + public static HttpClientErrorException create(@Nullable String message, HttpStatus statusCode, + String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { switch (statusCode) { case BAD_REQUEST: - return new HttpClientErrorException.BadRequest(message, statusText, headers, body, charset, url, method); + return message != null ? + new HttpClientErrorException.BadRequest(message, statusText, headers, body, charset) : + new HttpClientErrorException.BadRequest(statusText, headers, body, charset); case UNAUTHORIZED: - return new HttpClientErrorException.Unauthorized(message, statusText, headers, body, charset, url, method); + return message != null ? + new HttpClientErrorException.Unauthorized(message, statusText, headers, body, charset) : + new HttpClientErrorException.Unauthorized(statusText, headers, body, charset); case FORBIDDEN: - return new HttpClientErrorException.Forbidden(message, statusText, headers, body, charset, url, method); + return message != null ? + new HttpClientErrorException.Forbidden(message, statusText, headers, body, charset) : + new HttpClientErrorException.Forbidden(statusText, headers, body, charset); case NOT_FOUND: - return new HttpClientErrorException.NotFound(message, statusText, headers, body, charset, url, method); + return message != null ? + new HttpClientErrorException.NotFound(message, statusText, headers, body, charset) : + new HttpClientErrorException.NotFound(statusText, headers, body, charset); case METHOD_NOT_ALLOWED: - return new HttpClientErrorException.MethodNotAllowed(message, statusText, headers, body, charset, url, method); + return message != null ? + new HttpClientErrorException.MethodNotAllowed(message, statusText, headers, body, charset) : + new HttpClientErrorException.MethodNotAllowed(statusText, headers, body, charset); case NOT_ACCEPTABLE: - return new HttpClientErrorException.NotAcceptable(message, statusText, headers, body, charset, url, method); + return message != null ? + new HttpClientErrorException.NotAcceptable(message, statusText, headers, body, charset) : + new HttpClientErrorException.NotAcceptable(statusText, headers, body, charset); case CONFLICT: - return new HttpClientErrorException.Conflict(message, statusText, headers, body, charset, url, method); + return message != null ? + new HttpClientErrorException.Conflict(message, statusText, headers, body, charset) : + new HttpClientErrorException.Conflict(statusText, headers, body, charset); case GONE: - return new HttpClientErrorException.Gone(message, statusText, headers, body, charset, url, method); + return message != null ? + new HttpClientErrorException.Gone(message, statusText, headers, body, charset) : + new HttpClientErrorException.Gone(statusText, headers, body, charset); case UNSUPPORTED_MEDIA_TYPE: - return new HttpClientErrorException.UnsupportedMediaType(message, statusText, headers, body, charset, url, method); + return message != null ? + new HttpClientErrorException.UnsupportedMediaType(message, statusText, headers, body, charset) : + new HttpClientErrorException.UnsupportedMediaType(statusText, headers, body, charset); case TOO_MANY_REQUESTS: - return new HttpClientErrorException.TooManyRequests(message, statusText, headers, body, charset, url, method); + return message != null ? + new HttpClientErrorException.TooManyRequests(message, statusText, headers, body, charset) : + new HttpClientErrorException.TooManyRequests(statusText, headers, body, charset); case UNPROCESSABLE_ENTITY: - return new HttpClientErrorException.UnprocessableEntity(message, statusText, headers, body, charset, url, method); + return message != null ? + new HttpClientErrorException.UnprocessableEntity(message, statusText, headers, body, charset) : + new HttpClientErrorException.UnprocessableEntity(statusText, headers, body, charset); default: - return new HttpClientErrorException(message, statusCode, statusText, headers, body, charset, url, method); + return message != null ? + new HttpClientErrorException(message, statusCode, statusText, headers, body, charset) : + new HttpClientErrorException(statusCode, statusText, headers, body, charset); } } @@ -122,12 +156,16 @@ public static HttpClientErrorException create( * @since 5.1 */ @SuppressWarnings("serial") - public static class BadRequest extends HttpClientErrorException { + public static final class BadRequest extends HttpClientErrorException { - BadRequest(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, - @Nullable URI url, @Nullable HttpMethod method) { + private BadRequest(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { + super(HttpStatus.BAD_REQUEST, statusText, headers, body, charset); + } + + private BadRequest(String message, String statusText, + HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(message, HttpStatus.BAD_REQUEST, statusText, headers, body, charset, url, method); + super(message, HttpStatus.BAD_REQUEST, statusText, headers, body, charset); } } @@ -136,12 +174,16 @@ public static class BadRequest extends HttpClientErrorException { * @since 5.1 */ @SuppressWarnings("serial") - public static class Unauthorized extends HttpClientErrorException { + public static final class Unauthorized extends HttpClientErrorException { + + private 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) { + private Unauthorized(String message, String statusText, + HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(message, HttpStatus.UNAUTHORIZED, statusText, headers, body, charset, url, method); + super(message, HttpStatus.UNAUTHORIZED, statusText, headers, body, charset); } } @@ -150,12 +192,16 @@ public static class Unauthorized extends HttpClientErrorException { * @since 5.1 */ @SuppressWarnings("serial") - public static class Forbidden extends HttpClientErrorException { + public static final class Forbidden extends HttpClientErrorException { - Forbidden(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, - @Nullable URI url, @Nullable HttpMethod method) { + private Forbidden(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { + super(HttpStatus.FORBIDDEN, statusText, headers, body, charset); + } + + private Forbidden(String message, String statusText, + HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(message, HttpStatus.FORBIDDEN, statusText, headers, body, charset, url, method); + super(message, HttpStatus.FORBIDDEN, statusText, headers, body, charset); } } @@ -164,12 +210,16 @@ public static class Forbidden extends HttpClientErrorException { * @since 5.1 */ @SuppressWarnings("serial") - public static class NotFound extends HttpClientErrorException { + public static final class NotFound extends HttpClientErrorException { - NotFound(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, - @Nullable URI url, @Nullable HttpMethod method) { + private NotFound(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { + super(HttpStatus.NOT_FOUND, statusText, headers, body, charset); + } - super(message, HttpStatus.NOT_FOUND, statusText, headers, body, charset, url, method); + private NotFound(String message, String statusText, + HttpHeaders headers, byte[] body, @Nullable Charset charset) { + + super(message, HttpStatus.NOT_FOUND, statusText, headers, body, charset); } } @@ -178,12 +228,16 @@ public static class NotFound extends HttpClientErrorException { * @since 5.1 */ @SuppressWarnings("serial") - public static class MethodNotAllowed extends HttpClientErrorException { + public static final class MethodNotAllowed extends HttpClientErrorException { + + private 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) { + private MethodNotAllowed(String message, String statusText, + HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(message, HttpStatus.METHOD_NOT_ALLOWED, statusText, headers, body, charset, url, method); + super(message, HttpStatus.METHOD_NOT_ALLOWED, statusText, headers, body, charset); } } @@ -192,12 +246,16 @@ public static class MethodNotAllowed extends HttpClientErrorException { * @since 5.1 */ @SuppressWarnings("serial") - public static class NotAcceptable extends HttpClientErrorException { + public static final class NotAcceptable extends HttpClientErrorException { - NotAcceptable(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, - @Nullable URI url, @Nullable HttpMethod method) { + private NotAcceptable(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { + super(HttpStatus.NOT_ACCEPTABLE, statusText, headers, body, charset); + } - super(message, HttpStatus.NOT_ACCEPTABLE, statusText, headers, body, charset, url, method); + private NotAcceptable(String message, String statusText, + HttpHeaders headers, byte[] body, @Nullable Charset charset) { + + super(message, HttpStatus.NOT_ACCEPTABLE, statusText, headers, body, charset); } } @@ -206,12 +264,14 @@ public static class NotAcceptable extends HttpClientErrorException { * @since 5.1 */ @SuppressWarnings("serial") - public static class Conflict extends HttpClientErrorException { + public static final class Conflict extends HttpClientErrorException { - Conflict(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, - @Nullable URI url, @Nullable HttpMethod method) { + private Conflict(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { + super(HttpStatus.CONFLICT, statusText, headers, body, charset); + } - super(message, HttpStatus.CONFLICT, statusText, headers, body, charset, url, method); + private Conflict(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { + super(message, HttpStatus.CONFLICT, statusText, headers, body, charset); } } @@ -220,12 +280,14 @@ public static class Conflict extends HttpClientErrorException { * @since 5.1 */ @SuppressWarnings("serial") - public static class Gone extends HttpClientErrorException { + public static final class Gone extends HttpClientErrorException { - Gone(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, - @Nullable URI url, @Nullable HttpMethod method) { + private Gone(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { + super(HttpStatus.GONE, statusText, headers, body, charset); + } - super(message, HttpStatus.GONE, statusText, headers, body, charset, url, method); + private Gone(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { + super(message, HttpStatus.GONE, statusText, headers, body, charset); } } @@ -234,12 +296,16 @@ public static class Gone extends HttpClientErrorException { * @since 5.1 */ @SuppressWarnings("serial") - public static class UnsupportedMediaType extends HttpClientErrorException { + public static final class UnsupportedMediaType extends HttpClientErrorException { - UnsupportedMediaType(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, - @Nullable URI url, @Nullable HttpMethod method) { + private UnsupportedMediaType(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { + super(HttpStatus.UNSUPPORTED_MEDIA_TYPE, statusText, headers, body, charset); + } + + private UnsupportedMediaType(String message, String statusText, + HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(message, HttpStatus.UNSUPPORTED_MEDIA_TYPE, statusText, headers, body, charset, url, method); + super(message, HttpStatus.UNSUPPORTED_MEDIA_TYPE, statusText, headers, body, charset); } } @@ -248,12 +314,16 @@ public static class UnsupportedMediaType extends HttpClientErrorException { * @since 5.1 */ @SuppressWarnings("serial") - public static class UnprocessableEntity extends HttpClientErrorException { + public static final class UnprocessableEntity extends HttpClientErrorException { + + private 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) { + private UnprocessableEntity(String message, String statusText, + HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(message, HttpStatus.UNPROCESSABLE_ENTITY, statusText, headers, body, charset, url, method); + super(message, HttpStatus.UNPROCESSABLE_ENTITY, statusText, headers, body, charset); } } @@ -262,12 +332,16 @@ public static class UnprocessableEntity extends HttpClientErrorException { * @since 5.1 */ @SuppressWarnings("serial") - public static class TooManyRequests extends HttpClientErrorException { + public static final class TooManyRequests extends HttpClientErrorException { + + private 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) { + private TooManyRequests(String message, String statusText, + HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(message, HttpStatus.TOO_MANY_REQUESTS, statusText, headers, body, charset, url, method); + super(message, HttpStatus.TOO_MANY_REQUESTS, statusText, headers, body, charset); } } 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 deleted file mode 100644 index d141549c8569..000000000000 --- a/spring-web/src/main/java/org/springframework/web/client/HttpErrorDetailsExtractor.java +++ /dev/null @@ -1,33 +0,0 @@ -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 4a63be5cf160..d45eb16da9ce 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,9 @@ 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; @@ -69,35 +67,59 @@ public HttpServerErrorException(HttpStatus statusCode, String statusText, } /** - * Constructor with a status code and status text, headers, content, request URL, and method. + * Constructor with a status code and status text, headers, content, and an + * prepared message. + * @since 5.2.2 */ 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); + @Nullable HttpHeaders headers, @Nullable byte[] body, @Nullable Charset charset) { + + super(message, statusCode, statusText, headers, body, charset); } /** * Create an {@code HttpServerErrorException} or an HTTP status specific sub-class. * @since 5.1 */ - public static HttpServerErrorException create( - String message, HttpStatus statusCode, String statusText, HttpHeaders headers, byte[] body, - @Nullable Charset charset, @Nullable URI url, @Nullable HttpMethod method) { + public static HttpServerErrorException create(HttpStatus statusCode, + String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { + + return create(null, statusCode, statusText, headers, body, charset); + } + + /** + * Variant of {@link #create(String, HttpStatus, String, HttpHeaders, byte[], Charset)} + * with an optional prepared message. + * @since 5.2.2. + */ + public static HttpServerErrorException create(@Nullable String message, HttpStatus statusCode, + String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { switch (statusCode) { case INTERNAL_SERVER_ERROR: - return new HttpServerErrorException.InternalServerError(message, statusText, headers, body, charset, url, method); + return message != null ? + new HttpServerErrorException.InternalServerError(message, statusText, headers, body, charset) : + new HttpServerErrorException.InternalServerError(statusText, headers, body, charset); case NOT_IMPLEMENTED: - return new HttpServerErrorException.NotImplemented(message, statusText, headers, body, charset, url, method); + return message != null ? + new HttpServerErrorException.NotImplemented(message, statusText, headers, body, charset) : + new HttpServerErrorException.NotImplemented(statusText, headers, body, charset); case BAD_GATEWAY: - return new HttpServerErrorException.BadGateway(message, statusText, headers, body, charset, url, method); + return message != null ? + new HttpServerErrorException.BadGateway(message, statusText, headers, body, charset) : + new HttpServerErrorException.BadGateway(statusText, headers, body, charset); case SERVICE_UNAVAILABLE: - return new HttpServerErrorException.ServiceUnavailable(message, statusText, headers, body, charset, url, method); + return message != null ? + new HttpServerErrorException.ServiceUnavailable(message, statusText, headers, body, charset) : + new HttpServerErrorException.ServiceUnavailable(statusText, headers, body, charset); case GATEWAY_TIMEOUT: - return new HttpServerErrorException.GatewayTimeout(message, statusText, headers, body, charset, url, method); + return message != null ? + new HttpServerErrorException.GatewayTimeout(message, statusText, headers, body, charset) : + new HttpServerErrorException.GatewayTimeout(statusText, headers, body, charset); default: - return new HttpServerErrorException(message, statusCode, statusText, headers, body, charset, url, method); + return message != null ? + new HttpServerErrorException(message, statusCode, statusText, headers, body, charset) : + new HttpServerErrorException(statusCode, statusText, headers, body, charset); } } @@ -109,12 +131,16 @@ public static HttpServerErrorException create( * @since 5.1 */ @SuppressWarnings("serial") - public static class InternalServerError extends HttpServerErrorException { + public static final class InternalServerError extends HttpServerErrorException { + + private 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) { + private InternalServerError(String message, String statusText, + HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(message, HttpStatus.INTERNAL_SERVER_ERROR, statusText, headers, body, charset, url, method); + super(message, HttpStatus.INTERNAL_SERVER_ERROR, statusText, headers, body, charset); } } @@ -123,12 +149,16 @@ public static class InternalServerError extends HttpServerErrorException { * @since 5.1 */ @SuppressWarnings("serial") - public static class NotImplemented extends HttpServerErrorException { + public static final class NotImplemented extends HttpServerErrorException { - NotImplemented(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, - @Nullable URI url, @Nullable HttpMethod method) { + private NotImplemented(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { + super(HttpStatus.NOT_IMPLEMENTED, statusText, headers, body, charset); + } + + private NotImplemented(String message, String statusText, + HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(message, HttpStatus.NOT_IMPLEMENTED, statusText, headers, body, charset, url, method); + super(message, HttpStatus.NOT_IMPLEMENTED, statusText, headers, body, charset); } } @@ -137,12 +167,16 @@ public static class NotImplemented extends HttpServerErrorException { * @since 5.1 */ @SuppressWarnings("serial") - public static class BadGateway extends HttpServerErrorException { + public static final class BadGateway extends HttpServerErrorException { + + private 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) { + private BadGateway(String message, String statusText, + HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(message, HttpStatus.BAD_GATEWAY, statusText, headers, body, charset, url, method); + super(message, HttpStatus.BAD_GATEWAY, statusText, headers, body, charset); } } @@ -151,12 +185,16 @@ public static class BadGateway extends HttpServerErrorException { * @since 5.1 */ @SuppressWarnings("serial") - public static class ServiceUnavailable extends HttpServerErrorException { + public static final class ServiceUnavailable extends HttpServerErrorException { - ServiceUnavailable(String message, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset, - @Nullable URI url, @Nullable HttpMethod method) { + private ServiceUnavailable(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) { + super(HttpStatus.SERVICE_UNAVAILABLE, statusText, headers, body, charset); + } + + private ServiceUnavailable(String message, String statusText, + HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(message, HttpStatus.SERVICE_UNAVAILABLE, statusText, headers, body, charset, url, method); + super(message, HttpStatus.SERVICE_UNAVAILABLE, statusText, headers, body, charset); } } @@ -165,12 +203,16 @@ public static class ServiceUnavailable extends HttpServerErrorException { * @since 5.1 */ @SuppressWarnings("serial") - public static class GatewayTimeout extends HttpServerErrorException { + public static final class GatewayTimeout extends HttpServerErrorException { + + private 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) { + private GatewayTimeout(String message, String statusText, + HttpHeaders headers, byte[] body, @Nullable Charset charset) { - super(message, HttpStatus.GATEWAY_TIMEOUT, statusText, headers, body, charset, url, method); + super(message, HttpStatus.GATEWAY_TIMEOUT, statusText, headers, body, charset); } } 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 726e77558737..b660f15be870 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,11 +16,9 @@ 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; import org.springframework.util.StringUtils; @@ -85,8 +83,8 @@ protected HttpStatusCodeException(HttpStatus statusCode, String statusText, protected HttpStatusCodeException(HttpStatus statusCode, String statusText, @Nullable HttpHeaders responseHeaders, @Nullable byte[] responseBody, @Nullable Charset responseCharset) { - this(getMessage(statusCode, statusText), statusCode, statusText, - responseHeaders, responseBody, responseCharset, null, null); + this(getMessage(statusCode, statusText), + statusCode, statusText, responseHeaders, responseBody, responseCharset); } /** @@ -98,15 +96,12 @@ protected HttpStatusCodeException(HttpStatus statusCode, String statusText, * @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} + * @since 5.2.2 */ protected HttpStatusCodeException(String message, HttpStatus statusCode, String statusText, - @Nullable HttpHeaders responseHeaders, @Nullable byte[] responseBody, @Nullable Charset responseCharset, - @Nullable URI url, @Nullable HttpMethod method) { + @Nullable HttpHeaders responseHeaders, @Nullable byte[] responseBody, @Nullable Charset responseCharset) { - super(message, statusCode.value(), statusText, responseHeaders, responseBody, - responseCharset, url, method); + super(message, statusCode.value(), statusText, responseHeaders, responseBody, responseCharset); 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 5410096477a4..2a927f376905 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,12 +17,10 @@ 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; /** @@ -50,12 +48,6 @@ 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. @@ -67,22 +59,6 @@ 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; @@ -90,8 +66,6 @@ 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; } @@ -124,20 +98,6 @@ 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 converted to String. The charset used is that * of the response "Content-Type" or otherwise {@code "UTF-8"}. 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 79d5544ad022..c7b0319018d4 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,9 @@ 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; @@ -48,7 +46,7 @@ public UnknownHttpStatusCodeException(int rawStatusCode, String statusText, @Nul @Nullable byte[] responseBody, @Nullable Charset responseCharset) { this("Unknown status code [" + rawStatusCode + "]" + " " + statusText, - rawStatusCode, statusText, responseHeaders, responseBody, responseCharset, null, null); + rawStatusCode, statusText, responseHeaders, responseBody, responseCharset); } /** @@ -59,12 +57,11 @@ public UnknownHttpStatusCodeException(int rawStatusCode, String statusText, @Nul * @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}) + * @since 5.2.2 */ - public UnknownHttpStatusCodeException(String message, int rawStatusCode, String statusText, @Nullable HttpHeaders responseHeaders, - @Nullable byte[] responseBody, @Nullable Charset responseCharset, @Nullable URI url, @Nullable HttpMethod method) { + public UnknownHttpStatusCodeException(String message, int rawStatusCode, String statusText, + @Nullable HttpHeaders responseHeaders, @Nullable byte[] responseBody, @Nullable Charset responseCharset) { - super(message, rawStatusCode, statusText, responseHeaders, responseBody, responseCharset, url, method); + super(message, rawStatusCode, statusText, responseHeaders, responseBody, responseCharset); } } 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 deleted file mode 100644 index 94804ac69f8a..000000000000 --- a/spring-web/src/test/java/org/springframework/web/client/DefaultHttpErrorDetailsExtractorTests.java +++ /dev/null @@ -1,97 +0,0 @@ -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/DefaultResponseErrorHandlerTests.java b/spring-web/src/test/java/org/springframework/web/client/DefaultResponseErrorHandlerTests.java index e9f782609378..c5c05c48e426 100644 --- a/spring-web/src/test/java/org/springframework/web/client/DefaultResponseErrorHandlerTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/DefaultResponseErrorHandlerTests.java @@ -19,8 +19,10 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.function.Function; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -69,9 +71,28 @@ public void handleError() throws Exception { given(response.getHeaders()).willReturn(headers); given(response.getBody()).willReturn(new ByteArrayInputStream("Hello World".getBytes(StandardCharsets.UTF_8))); - assertThatExceptionOfType(HttpClientErrorException.class).isThrownBy(() -> - handler.handleError(response)) - .satisfies(ex -> assertThat(ex.getResponseHeaders()).isSameAs(headers)); + assertThatExceptionOfType(HttpClientErrorException.class) + .isThrownBy(() -> handler.handleError(response)) + .satisfies(ex -> assertThat(ex.getResponseHeaders()).isSameAs(headers)) + .satisfies(ex -> assertThat(ex.getMessage()).isEqualTo("404 Not Found: [Hello World]")); + } + + @Test + public void handleErrorWithLongBody() throws Exception { + + Function bodyGenerator = + size -> Flux.just("a").repeat(size-1).reduce((s, s2) -> s + s2).block(); + + given(response.getRawStatusCode()).willReturn(HttpStatus.NOT_FOUND.value()); + given(response.getStatusText()).willReturn("Not Found"); + given(response.getHeaders()).willReturn(new HttpHeaders()); + given(response.getBody()).willReturn( + new ByteArrayInputStream(bodyGenerator.apply(500).getBytes(StandardCharsets.UTF_8))); + + assertThatExceptionOfType(HttpClientErrorException.class) + .isThrownBy(() -> handler.handleError(response)) + .satisfies(ex -> assertThat(ex.getMessage()).isEqualTo( + "404 Not Found: [" + bodyGenerator.apply(200) + "... (500 bytes)]")); } @Test @@ -84,8 +105,7 @@ public void handleErrorIOException() throws Exception { given(response.getHeaders()).willReturn(headers); given(response.getBody()).willThrow(new IOException()); - assertThatExceptionOfType(HttpClientErrorException.class).isThrownBy(() -> - handler.handleError(response)); + assertThatExceptionOfType(HttpClientErrorException.class).isThrownBy(() -> handler.handleError(response)); } @Test 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 631725b6bd29..f53cf890c79b 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 @@ -254,8 +254,7 @@ void badRequest(ClientHttpRequestFactory clientHttpRequestFactory) { template.execute(baseUrl + "/status/badrequest", HttpMethod.GET, null, null)) .satisfies(ex -> { assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); - assertThat(ex.getMessage()).isEqualTo( - "400 Client Error after GET http://localhost:" + port + "/status/badrequest : [no body]"); + assertThat(ex.getMessage()).isEqualTo("400 Client Error: [no body]"); }); }