Skip to content

Instantly share code, notes, and snippets.

@jkuipers
Last active March 22, 2025 10:04
Show Gist options
  • Save jkuipers/3149d21932784ea0ae08494d5f6fa2ba to your computer and use it in GitHub Desktop.
Save jkuipers/3149d21932784ea0ae08494d5f6fa2ba to your computer and use it in GitHub Desktop.
Spring ResponseErrorHandler for JSON error responses
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