Last active
March 22, 2025 10:04
-
-
Save jkuipers/3149d21932784ea0ae08494d5f6fa2ba to your computer and use it in GitHub Desktop.
Spring ResponseErrorHandler for JSON error responses
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.fasterxml.jackson.databind.ObjectMapper; | |
import com.fasterxml.jackson.databind.type.TypeFactory; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
import org.springframework.http.HttpMethod; | |
import org.springframework.http.HttpStatusCode; | |
import org.springframework.http.MediaType; | |
import org.springframework.http.client.ClientHttpResponse; | |
import org.springframework.lang.NonNull; | |
import org.springframework.web.client.DefaultResponseErrorHandler; | |
import java.io.IOException; | |
import java.lang.reflect.ParameterizedType; | |
import java.net.URI; | |
import static java.nio.charset.StandardCharsets.UTF_8; | |
import static org.springframework.http.MediaType.APPLICATION_JSON; | |
/** | |
* Allows for handling errors that return a JSON body easily, with | |
* fallback to default error handling in case the JSON cannot be parsed. | |
* | |
* @param <T> type to unmarshal to, e.g. {@code Map<String, Object>} | |
*/ | |
public abstract class AbstractJsonResponseErrorHandler<T> extends DefaultResponseErrorHandler { | |
protected ObjectMapper objectMapper; | |
protected Logger logger = LoggerFactory.getLogger(getClass()); | |
private Class<T> valueType; | |
public AbstractJsonResponseErrorHandler(ObjectMapper objectMapper) { | |
this.objectMapper = objectMapper; | |
// fighting type erasure here so that we can unmarshal to our parameterized T | |
ParameterizedType superClass = (ParameterizedType) getClass().getGenericSuperclass(); | |
this.valueType = (Class<T>) TypeFactory.rawClass(superClass.getActualTypeArguments()[0]); | |
} | |
@Override | |
protected void handleError(ClientHttpResponse response, HttpStatusCode statusCode, URI url, HttpMethod method) throws IOException { | |
MediaType mimeType = response.getHeaders().getContentType(); | |
if (mimeType == null || !isJson(mimeType) || useDefaultHandling(response)) { | |
super.handleError(response, statusCode, url, method); | |
} | |
byte[] responseBody = getResponseBody(response); | |
try { | |
T responseJson = objectMapper.readValue(responseBody, valueType); | |
handleJsonError(responseJson, statusCode); | |
} catch (IOException e) { | |
logger.warn("Couldn't parse JSON error response '{}': {}", | |
new String(responseBody, UTF_8), e.getMessage()); | |
} | |
super.handleError(new BufferedClientHttpResponseWrapper(response, responseBody), statusCode, url, method); | |
} | |
/** | |
* Determines if we consider the provided {@link MediaType} to represent a JSON error response that we can handle. | |
* Defaults to check for compatibility with {@code application/json} and types ending in {@code +json}. | |
* Can be overridden to match a more narrow definition. | |
* | |
* @param mediaType never null | |
* @return whether to treat the response's Content-Type as JSON to handle | |
*/ | |
protected boolean isJson(@NonNull MediaType mediaType) { | |
return mediaType.isCompatibleWith(APPLICATION_JSON) || "json".equals(mediaType.getSubtypeSuffix()); | |
} | |
/** | |
* Return {@code true} to let the {@link DefaultResponseErrorHandler} handle the error, | |
* even though the response contains JSON, before the JSON is parsed. | |
* Returns {@code false} by default. | |
* Do NOT read the response's body in this method! | |
* | |
* @param response | |
*/ | |
protected boolean useDefaultHandling(ClientHttpResponse response) throws IOException { | |
return false; | |
} | |
/** | |
* Handle the error by throwing some unchecked exception. | |
* If no exception is thrown, fall back to Spring's default error handling. | |
* | |
* @param responseBody | |
* @param statusCode | |
*/ | |
protected abstract void handleJsonError(T responseBody, HttpStatusCode statusCode); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment