Consistent tolerance of unknown HTTP status codes behind RestTemplate

Issue: SPR-15978
This commit is contained in:
Juergen Hoeller
2017-09-20 10:54:58 +02:00
parent 215e5f5682
commit 4cbef27f90
14 changed files with 155 additions and 151 deletions

View File

@@ -16,6 +16,8 @@
package org.springframework.http;
import org.springframework.lang.Nullable;
/**
* Enumeration of HTTP status codes.
*
@@ -499,12 +501,28 @@ public enum HttpStatus {
* @throws IllegalArgumentException if this enum has no constant for the specified numeric value
*/
public static HttpStatus valueOf(int statusCode) {
HttpStatus status = resolve(statusCode);
if (status == null) {
throw new IllegalArgumentException("No matching constant for [" + statusCode + "]");
}
return status;
}
/**
* Resolve the given status code to an {@code HttpStatus}, if possible.
* @param statusCode the HTTP status code (potentially non-standard)
* @return the corresponding {@code HttpStatus}, or {@code null} if not found
* @since 5.0
*/
@Nullable
public static HttpStatus resolve(int statusCode) {
for (HttpStatus status : values()) {
if (status.value == statusCode) {
return status;
}
}
throw new IllegalArgumentException("No matching constant for [" + statusCode + "]");
return null;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2017 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.
@@ -38,13 +38,19 @@ public interface ClientHttpResponse extends HttpInputMessage, Closeable {
* Return the HTTP status code of the response.
* @return the HTTP status as an HttpStatus enum value
* @throws IOException in case of I/O errors
* @throws IllegalArgumentException in case of an unknown HTTP status code
* @see HttpStatus#valueOf(int)
*/
HttpStatus getStatusCode() throws IOException;
/**
* Return the HTTP status code of the response as integer
* Return the HTTP status code (potentially non-standard and not
* resolvable through the {@link HttpStatus} enum) as an integer.
* @return the HTTP status as an integer
* @throws IOException in case of I/O errors
* @since 3.1.1
* @see #getStatusCode()
* @see HttpStatus#resolve(int)
*/
int getRawStatusCode() throws IOException;

View File

@@ -523,7 +523,7 @@ public class AsyncRestTemplate extends org.springframework.http.client.support.I
if (logger.isDebugEnabled()) {
try {
logger.debug("Async " + method.name() + " request for \"" + url + "\" resulted in " +
response.getStatusCode() + " (" + response.getStatusText() + ")");
response.getRawStatusCode() + " (" + response.getStatusText() + ")");
}
catch (IOException ex) {
// ignore
@@ -535,7 +535,7 @@ public class AsyncRestTemplate extends org.springframework.http.client.support.I
if (logger.isWarnEnabled()) {
try {
logger.warn("Async " + method.name() + " request for \"" + url + "\" resulted in " +
response.getStatusCode() + " (" + response.getStatusText() + "); invoking error handler");
response.getRawStatusCode() + " (" + response.getStatusText() + "); invoking error handler");
}
catch (IOException ex) {
// ignore

View File

@@ -46,18 +46,46 @@ public class DefaultResponseErrorHandler implements ResponseErrorHandler {
*/
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return hasError(getHttpStatusCode(response));
HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode());
return (statusCode != null && hasError(statusCode));
}
/**
* 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.
* Template method called from {@link #hasError(ClientHttpResponse)}.
* <p>The default implementation checks if the given status code is
* {@link HttpStatus.Series#CLIENT_ERROR CLIENT_ERROR} or
* {@link HttpStatus.Series#SERVER_ERROR SERVER_ERROR}.
* Can be overridden in subclasses.
* @param statusCode the HTTP status code
* @return {@code true} if the response has an error; {@code false} otherwise
*/
protected boolean hasError(HttpStatus statusCode) {
return (statusCode.series() == HttpStatus.Series.CLIENT_ERROR ||
statusCode.series() == HttpStatus.Series.SERVER_ERROR);
}
/**
* Delegates to {@link #handleError(ClientHttpResponse, HttpStatus)} with the response status code.
*/
@Override
public void handleError(ClientHttpResponse response) throws IOException {
HttpStatus statusCode = getHttpStatusCode(response);
HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode());
if (statusCode == null) {
throw new UnknownHttpStatusCodeException(response.getRawStatusCode(), response.getStatusText(),
response.getHeaders(), getResponseBody(response), getCharset(response));
}
handleError(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(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
switch (statusCode.series()) {
case CLIENT_ERROR:
throw new HttpClientErrorException(statusCode, response.getStatusText(),
@@ -66,7 +94,8 @@ public class DefaultResponseErrorHandler implements ResponseErrorHandler {
throw new HttpServerErrorException(statusCode, response.getStatusText(),
response.getHeaders(), getResponseBody(response), getCharset(response));
default:
throw new RestClientException("Unknown status code [" + statusCode + "]");
throw new UnknownHttpStatusCodeException(statusCode.value(), response.getStatusText(),
response.getHeaders(), getResponseBody(response), getCharset(response));
}
}
@@ -79,43 +108,16 @@ public class DefaultResponseErrorHandler implements ResponseErrorHandler {
* @throws UnknownHttpStatusCodeException in case of an unknown status code
* that cannot be represented with the {@link HttpStatus} enum
* @since 4.3.8
* @deprecated as of 5.0, in favor of {@link #handleError(ClientHttpResponse, HttpStatus)}
*/
@Deprecated
protected HttpStatus getHttpStatusCode(ClientHttpResponse response) throws IOException {
try {
return response.getStatusCode();
}
catch (IllegalArgumentException ex) {
HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode());
if (statusCode == null) {
throw new UnknownHttpStatusCodeException(response.getRawStatusCode(), response.getStatusText(),
response.getHeaders(), getResponseBody(response), getCharset(response));
}
}
/**
* Template method called from {@link #hasError(ClientHttpResponse)}.
* <p>The default implementation checks if the given status code is
* {@link org.springframework.http.HttpStatus.Series#CLIENT_ERROR CLIENT_ERROR}
* or {@link org.springframework.http.HttpStatus.Series#SERVER_ERROR SERVER_ERROR}.
* Can be overridden in subclasses.
* @param statusCode the HTTP status code
* @return {@code true} if the response has an error; {@code false} otherwise
* @see #getHttpStatusCode(ClientHttpResponse)
*/
protected boolean hasError(HttpStatus statusCode) {
return (statusCode.series() == HttpStatus.Series.CLIENT_ERROR ||
statusCode.series() == HttpStatus.Series.SERVER_ERROR);
}
/**
* Determine the charset of the response (for inclusion in a status exception).
* @param response the response to inspect
* @return the associated charset, or {@code null} if none
* @since 4.3.8
*/
@Nullable
protected Charset getCharset(ClientHttpResponse response) {
HttpHeaders headers = response.getHeaders();
MediaType contentType = headers.getContentType();
return (contentType != null ? contentType.getCharset() : null);
return statusCode;
}
/**
@@ -135,4 +137,17 @@ public class DefaultResponseErrorHandler implements ResponseErrorHandler {
return new byte[0];
}
/**
* Determine the charset of the response (for inclusion in a status exception).
* @param response the response to inspect
* @return the associated charset, or {@code null} if none
* @since 4.3.8
*/
@Nullable
protected Charset getCharset(ClientHttpResponse response) {
HttpHeaders headers = response.getHeaders();
MediaType contentType = headers.getContentType();
return (contentType != null ? contentType.getCharset() : null);
}
}

View File

@@ -132,8 +132,7 @@ public class ExtractingResponseErrorHandler extends DefaultResponseErrorHandler
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
HttpStatus statusCode = getHttpStatusCode(response);
public void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
if (this.statusMapping.containsKey(statusCode)) {
extract(this.statusMapping.get(statusCode), response);
}
@@ -141,7 +140,7 @@ public class ExtractingResponseErrorHandler extends DefaultResponseErrorHandler
extract(this.seriesMapping.get(statusCode.series()), response);
}
else {
super.handleError(response);
super.handleError(response, statusCode);
}
}

View File

@@ -58,12 +58,12 @@ class MessageBodyClientHttpResponseWrapper implements ClientHttpResponse {
* @throws IOException in case of I/O errors
*/
public boolean hasMessageBody() throws IOException {
HttpStatus responseStatus = this.getStatusCode();
if (responseStatus.is1xxInformational() || responseStatus == HttpStatus.NO_CONTENT ||
responseStatus == HttpStatus.NOT_MODIFIED) {
HttpStatus status = HttpStatus.resolve(getRawStatusCode());
if (status != null && (status.is1xxInformational() || status == HttpStatus.NO_CONTENT ||
status == HttpStatus.NOT_MODIFIED)) {
return false;
}
else if (getHeaders().getContentLength() == 0) {
if (getHeaders().getContentLength() == 0) {
return false;
}
return true;

View File

@@ -969,10 +969,10 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
public ResponseEntity<T> extractData(ClientHttpResponse response) throws IOException {
if (this.delegate != null) {
T body = this.delegate.extractData(response);
return new ResponseEntity<>(body, response.getHeaders(), response.getStatusCode());
return ResponseEntity.status(response.getRawStatusCode()).headers(response.getHeaders()).body(body);
}
else {
return new ResponseEntity<>(response.getHeaders(), response.getStatusCode());
return ResponseEntity.status(response.getRawStatusCode()).headers(response.getHeaders()).build();
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2017 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.
@@ -38,12 +38,12 @@ public class UnknownHttpStatusCodeException extends RestClientResponseException
* {@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 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})
*/
public UnknownHttpStatusCodeException(int rawStatusCode, String statusText,
@Nullable HttpHeaders responseHeaders, @Nullable byte[] responseBody,@Nullable Charset responseCharset) {
public UnknownHttpStatusCodeException(int rawStatusCode, String statusText, @Nullable HttpHeaders responseHeaders,
@Nullable byte[] responseBody, @Nullable Charset responseCharset) {
super("Unknown status code [" + String.valueOf(rawStatusCode) + "]" + " " + statusText,
rawStatusCode, statusText, responseHeaders, responseBody, responseCharset);