Browse Source

SPR-17130 http error details in the exception message

pull/23992/head
jerzykrlk 7 years ago committed by Rossen Stoyanchev
parent
commit
91ec274b10
  1. 138
      spring-web/src/main/java/org/springframework/web/client/DefaultHttpErrorDetailsExtractor.java
  2. 59
      spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java
  3. 104
      spring-web/src/main/java/org/springframework/web/client/HttpClientErrorException.java
  4. 33
      spring-web/src/main/java/org/springframework/web/client/HttpErrorDetailsExtractor.java
  5. 55
      spring-web/src/main/java/org/springframework/web/client/HttpServerErrorException.java
  6. 26
      spring-web/src/main/java/org/springframework/web/client/HttpStatusCodeException.java
  7. 40
      spring-web/src/main/java/org/springframework/web/client/RestClientResponseException.java
  8. 22
      spring-web/src/main/java/org/springframework/web/client/UnknownHttpStatusCodeException.java
  9. 97
      spring-web/src/test/java/org/springframework/web/client/DefaultHttpErrorDetailsExtractorTests.java
  10. 3
      spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java

138
spring-web/src/main/java/org/springframework/web/client/DefaultHttpErrorDetailsExtractor.java

@ -0,0 +1,138 @@ @@ -0,0 +1,138 @@
package org.springframework.web.client;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import org.jetbrains.annotations.NotNull;
import org.springframework.http.HttpMethod;
import org.springframework.lang.Nullable;
/**
* Spring's default implementation of the {@link HttpErrorDetailsExtractor} interface.
*
* <p>This extractor will compose a short summary of the http error response, including:
* <ul>
* <li>request URI
* <li>request method
* <li>a 200-character preview of the response body, unformatted
* </ul>
*
* An example:
* <pre>
* 404 Not Found after GET http://example.com:8080/my-endpoint : [{'id': 123, 'message': 'my very long... (500 bytes)]</code>
* </pre>
*
* @author Jerzy Krolak
* @since 5.1
* @see DefaultResponseErrorHandler#setHttpErrorDetailsExtractor(HttpErrorDetailsExtractor)
*/
public class DefaultHttpErrorDetailsExtractor implements HttpErrorDetailsExtractor {
private static final int MAX_BODY_BYTES_LENGTH = 400;
private static final int MAX_BODY_CHARS_LENGTH = 200;
/**
* Assemble a short summary of the HTTP error response.
* @param rawStatusCode HTTP status code
* @param statusText HTTP status text
* @param responseBody response body
* @param responseCharset response charset
* @param url request URI
* @param method request method
* @return error details string. Example: <pre>404 Not Found after GET http://example.com:8080/my-endpoint : [{'id': 123, 'message': 'my very long... (500 bytes)]</code></pre>
*/
@Override
@NotNull
public String getErrorDetails(int rawStatusCode, String statusText, @Nullable byte[] responseBody,
@Nullable Charset responseCharset, @Nullable URI url, @Nullable HttpMethod method) {
if (url == null || method == null) {
return getSimpleErrorDetails(rawStatusCode, statusText);
}
return getCompleteErrorDetails(rawStatusCode, statusText, responseBody, responseCharset, url, method);
}
@NotNull
private String getCompleteErrorDetails(int rawStatusCode, String statusText, @Nullable byte[] responseBody,
@Nullable Charset responseCharset, @Nullable URI url, @Nullable HttpMethod method) {
StringBuilder result = new StringBuilder();
result.append(getSimpleErrorDetails(rawStatusCode, statusText))
.append(" after ")
.append(method)
.append(" ")
.append(url)
.append(" : ");
if (responseBody == null || responseBody.length == 0) {
result.append("[no body]");
}
else {
result
.append("[")
.append(getResponseBody(responseBody, responseCharset))
.append("]");
}
return result.toString();
}
@NotNull
private String getSimpleErrorDetails(int rawStatusCode, String statusText) {
return rawStatusCode + " " + statusText;
}
private String getResponseBody(byte[] responseBody, @Nullable Charset responseCharset) {
Charset charset = getCharsetOrDefault(responseCharset);
if (responseBody.length < MAX_BODY_BYTES_LENGTH) {
return getCompleteResponseBody(responseBody, charset);
}
return getResponseBodyPreview(responseBody, charset);
}
@NotNull
private String getCompleteResponseBody(byte[] responseBody, Charset responseCharset) {
return new String(responseBody, responseCharset);
}
private String getResponseBodyPreview(byte[] responseBody, Charset responseCharset) {
try {
String bodyPreview = readBodyAsString(responseBody, responseCharset);
return bodyPreview + "... (" + responseBody.length + " bytes)";
}
catch (IOException e) {
// should never happen
throw new IllegalStateException(e);
}
}
@NotNull
private String readBodyAsString(byte[] responseBody, Charset responseCharset) throws IOException {
Reader reader = new InputStreamReader(new ByteArrayInputStream(responseBody), responseCharset);
CharBuffer result = CharBuffer.allocate(MAX_BODY_CHARS_LENGTH);
reader.read(result);
reader.close();
result.flip();
return result.toString();
}
private Charset getCharsetOrDefault(@Nullable Charset responseCharset) {
if (responseCharset == null) {
return StandardCharsets.ISO_8859_1;
}
return responseCharset;
}
}

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

@ -17,13 +17,16 @@ @@ -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 @@ import org.springframework.util.FileCopyUtils; @@ -43,6 +46,17 @@ import org.springframework.util.FileCopyUtils;
*/
public class DefaultResponseErrorHandler implements ResponseErrorHandler {
private HttpErrorDetailsExtractor httpErrorDetailsExtractor = new DefaultHttpErrorDetailsExtractor();
/**
* Set the error summary extractor.
* <p>By default, DefaultResponseErrorHandler uses a {@link DefaultHttpErrorDetailsExtractor}.
*/
public void setHttpErrorDetailsExtractor(HttpErrorDetailsExtractor httpErrorDetailsExtractor) {
Assert.notNull(httpErrorDetailsExtractor, "HttpErrorDetailsExtractor must not be null");
this.httpErrorDetailsExtractor = httpErrorDetailsExtractor;
}
/**
* Delegates to {@link #hasError(HttpStatus)} (for a standard status enum value) or
* {@link #hasError(int)} (for an unknown status code) with the response status code.
@ -87,19 +101,31 @@ public class DefaultResponseErrorHandler implements ResponseErrorHandler { @@ -87,19 +101,31 @@ public class DefaultResponseErrorHandler implements ResponseErrorHandler {
}
/**
* 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 class DefaultResponseErrorHandler implements ResponseErrorHandler { @@ -114,17 +140,34 @@ public class DefaultResponseErrorHandler implements ResponseErrorHandler {
* @see HttpServerErrorException#create
*/
protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
handleError(null, null, response, statusCode);
}
/**
* Handle the error in the given response with the given resolved status code.
* <p>This default implementation throws a {@link HttpClientErrorException} if the response status code
* is {@link org.springframework.http.HttpStatus.Series#CLIENT_ERROR}, a {@link HttpServerErrorException}
* if it is {@link org.springframework.http.HttpStatus.Series#SERVER_ERROR},
* and a {@link RestClientException} in other cases.
* @since 5.0
*/
protected void handleError(@Nullable URI url, @Nullable HttpMethod method, ClientHttpResponse response,
HttpStatus statusCode) throws IOException {
String statusText = response.getStatusText();
HttpHeaders headers = response.getHeaders();
byte[] body = getResponseBody(response);
Charset charset = getCharset(response);
String message = httpErrorDetailsExtractor.getErrorDetails(statusCode.value(), statusText, body, charset, url, method);
switch (statusCode.series()) {
case CLIENT_ERROR:
throw HttpClientErrorException.create(statusCode, statusText, headers, body, charset);
throw HttpClientErrorException.create(message, statusCode, statusText, headers, body, charset, url, method);
case SERVER_ERROR:
throw HttpServerErrorException.create(statusCode, statusText, headers, body, charset);
throw HttpServerErrorException.create(message, statusCode, statusText, headers, body, charset, url, method);
default:
throw new UnknownHttpStatusCodeException(statusCode.value(), statusText, headers, body, charset);
throw new UnknownHttpStatusCodeException(message, statusCode.value(), statusText, headers, body,
charset, url, method);
}
}

104
spring-web/src/main/java/org/springframework/web/client/HttpClientErrorException.java

@ -16,9 +16,11 @@ @@ -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 class HttpClientErrorException extends HttpStatusCodeException { @@ -66,39 +68,49 @@ public class HttpClientErrorException extends HttpStatusCodeException {
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 class HttpClientErrorException extends HttpStatusCodeException { @@ -112,8 +124,10 @@ public class HttpClientErrorException extends HttpStatusCodeException {
@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 class HttpClientErrorException extends HttpStatusCodeException { @@ -124,8 +138,10 @@ public class HttpClientErrorException extends HttpStatusCodeException {
@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 class HttpClientErrorException extends HttpStatusCodeException { @@ -136,8 +152,10 @@ public class HttpClientErrorException extends HttpStatusCodeException {
@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 class HttpClientErrorException extends HttpStatusCodeException { @@ -148,8 +166,10 @@ public class HttpClientErrorException extends HttpStatusCodeException {
@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 class HttpClientErrorException extends HttpStatusCodeException { @@ -160,8 +180,10 @@ public class HttpClientErrorException extends HttpStatusCodeException {
@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 class HttpClientErrorException extends HttpStatusCodeException { @@ -172,8 +194,10 @@ public class HttpClientErrorException extends HttpStatusCodeException {
@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 class HttpClientErrorException extends HttpStatusCodeException { @@ -184,8 +208,10 @@ public class HttpClientErrorException extends HttpStatusCodeException {
@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 class HttpClientErrorException extends HttpStatusCodeException { @@ -196,8 +222,10 @@ public class HttpClientErrorException extends HttpStatusCodeException {
@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 class HttpClientErrorException extends HttpStatusCodeException { @@ -208,8 +236,10 @@ public class HttpClientErrorException extends HttpStatusCodeException {
@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 class HttpClientErrorException extends HttpStatusCodeException { @@ -220,8 +250,10 @@ public class HttpClientErrorException extends HttpStatusCodeException {
@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 class HttpClientErrorException extends HttpStatusCodeException { @@ -232,8 +264,10 @@ public class HttpClientErrorException extends HttpStatusCodeException {
@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);
}
}

33
spring-web/src/main/java/org/springframework/web/client/HttpErrorDetailsExtractor.java

@ -0,0 +1,33 @@ @@ -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);
}

55
spring-web/src/main/java/org/springframework/web/client/HttpServerErrorException.java

@ -16,9 +16,11 @@ @@ -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 class HttpServerErrorException extends HttpStatusCodeException { @@ -66,27 +68,36 @@ public class HttpServerErrorException extends HttpStatusCodeException {
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 class HttpServerErrorException extends HttpStatusCodeException { @@ -100,8 +111,10 @@ public class HttpServerErrorException extends HttpStatusCodeException {
@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 class HttpServerErrorException extends HttpStatusCodeException { @@ -112,8 +125,10 @@ public class HttpServerErrorException extends HttpStatusCodeException {
@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 class HttpServerErrorException extends HttpStatusCodeException { @@ -124,8 +139,10 @@ public class HttpServerErrorException extends HttpStatusCodeException {
@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 class HttpServerErrorException extends HttpStatusCodeException { @@ -136,8 +153,10 @@ public class HttpServerErrorException extends HttpStatusCodeException {
@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 class HttpServerErrorException extends HttpStatusCodeException { @@ -148,8 +167,10 @@ public class HttpServerErrorException extends HttpStatusCodeException {
@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);
}
}

26
spring-web/src/main/java/org/springframework/web/client/HttpStatusCodeException.java

@ -16,9 +16,11 @@ @@ -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 @@ public abstract class HttpStatusCodeException extends RestClientResponseExceptio @@ -83,8 +85,28 @@ public abstract class HttpStatusCodeException extends RestClientResponseExceptio
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;
}

40
spring-web/src/main/java/org/springframework/web/client/RestClientResponseException.java

@ -17,10 +17,12 @@ @@ -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 { @@ -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 { @@ -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 class RestClientResponseException extends RestClientException { @@ -66,6 +90,8 @@ public class RestClientResponseException extends RestClientException {
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 class RestClientResponseException extends RestClientException { @@ -98,6 +124,20 @@ public class RestClientResponseException extends RestClientException {
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"}.

22
spring-web/src/main/java/org/springframework/web/client/UnknownHttpStatusCodeException.java

@ -16,9 +16,11 @@ @@ -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 @@ -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);
}
}

97
spring-web/src/test/java/org/springframework/web/client/DefaultHttpErrorDetailsExtractorTests.java

@ -0,0 +1,97 @@ @@ -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);
}
}

3
spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java

@ -254,7 +254,8 @@ class RestTemplateIntegrationTests extends AbstractMockWebServerTests { @@ -254,7 +254,8 @@ class RestTemplateIntegrationTests extends AbstractMockWebServerTests {
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]");
});
}

Loading…
Cancel
Save