I have the following REST endpoint:
@PostMapping(path = "/dispatchJob")
public ResponseEntity<Void> dispatchJob(@RequestBody(required = false) JobDispatchRequestBody dispatchRequestBody) throws FailedToAcceptJobException {
modelMapper.getConfiguration().setPropertyCondition(Conditions.isNotNull());
Job j = modelMapper.map(dispatchRequestBody.getDispatchRequestsDTO(), Job.class);
jobsSvc.assignJob(j, dispatchRequestBody.getDispatchEpoch());
return ResponseEntity.noContent().build();
}
The “jobsSvc.assignJob” function throws exception:
@Override
public synchronized void assignJob(Job j, Long dispatchEpoch) throws FailedToAcceptJobException {
initJobObject(j);
throw new FailedToAcceptJobException(new DispatchJobErrorResponse("System is shutting down, not accepting new job!",
DispatchJobErrorResponse.JobRejectionErrorCode.SystemISShuttingDown));
}
And the exception class is as follows:
@Data
public class FailedToAcceptJobException extends Exception {
private final DispatchJobErrorResponse dispatchJobErrorResponse;
public FailedToAcceptJobException(DispatchJobErrorResponse dispatchJobErrorResponse) {
super("Failed to assign job on agent exception: " + dispatchJobErrorResponse.getMessage());
this.dispatchJobErrorResponse = dispatchJobErrorResponse;
}
I created the following handler that is being called once exception is thrown within this endpoint (I validated that through test):
@ExceptionHandler(value = FailedToAcceptJobException.class)
public ResponseEntity<Object> handleCustomException(HttpServletRequest request,
FailedToAcceptJobException ex)
{
return new ResponseEntity<>(ex.getDispatchJobErrorResponse(),HttpStatus.BAD_REQUEST);
}
My issue is, when I’m trying to call this endpoint via post request, I don’t get the body and I can’t access the error object (this is a test).
String url = "/dispatchJob";
JobDispatchDTO jobDispatchDTO = new JobDispatchDTO();
JobDispatchRequestBody dispatchRequestBody = new JobDispatchRequestBody(jobDispatchDTO,1L);
ResponseEntity<Void> res = restTemplate.postForEntity(url, dispatchRequestBody,Void.class);
I’ve been playing with it for hours, I realize that eventually I’ll have to use object mapper in order to access “DispatchJobErrorResponse” fields, but the body remains null in the response entity no matter what I do.
Please assist
1
You’re thinking about this incorrectly. Your call to postForEntity
will throw a HttpStatusCodeException
because any 4xx response will cause RestTemplate
to throw an exception so there will be no body to deserialize into a ResponseEntity
.
To illustrate this, surround your call with a try/catch
try {
ResponseEntity<Void> res = restTemplate.postForEntity(url, dispatchRequestBody, Void.class);
} catch (HttpStatusCodeException e) {
String error = e.getMessage(); // should contain "400: Failed to assign job on agent exception: System is shutting down, not accepting new job!"
}
If you want to always return a consistent object that may contain errors, create a wrapper class for your domain that will encapsulate both your data and your errors. In your exception handler, always return a 200 with an instance of this wrapper.
The wrapper would look something like this:
@Data
public class ResponseWrapper <T> {
T data;
String errors;
boolean hasErrors;
}
You would modify your API to return a ResponseEntity of ResponseWrapper, like this:
public ResponseEntity<ResponseWrapper<Void>> dispatchJob...
Then modify your error handler to return the ResponseWrapper, with a status of ok.
@ExceptionHandler(value = FailedToAcceptJobException.class)
public ResponseWrapper<?> handleCustomException(HttpServletRequest request,
FailedToAcceptJobException ex)
{
ResponseWrapper<?> body = new ResponseWrapper<>();
body.setErrors(ex.getDispatchJobErrorResponse());
body.setHasErrors(true);
return ResponseEntity.ok.body(body);
}
On the calling side, it would look like this:
ParameterizedTypeReference<ResponseWrapper<Void>> responseWrapperParameterizedTypeReference = new ParameterizedTypeReference<>() {};
RequestEntity<Void> requestEntity = new RequestEntity<>(POST, new URI("url"));
ResponseEntity<ResponseWrapper<Void>> response = restTemplate.exchange(url, HttpMethod POST, dispatchRequestBody, responseWrapperParameterizedTypeReference);
// Now you can do what I believe you want:
if (response.getBody().hasErrors()) {
response.getBody().getErrors(); // should contain your errors
} else {
response.getBody().getData(); // would have the data that you set in your controller, if this wasn't a Void return type
}
On a simpler note, if you want to handle the errors from RestTemplate in a more concise manner, consider creating a Component
that implements ResponseErrorHandler
or extends DefaultResponseErrorHandler
. See this. A simple implementation would look something like this
import com.phistonemetals.api.exception.RestTemplateException;
import io.micrometer.core.instrument.util.IOUtils;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.client.DefaultResponseErrorHandler;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Component
public class RestTemplateResponseErrorHandler extends DefaultResponseErrorHandler {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return response.getStatusCode().is5xxServerError() ||
response.getStatusCode().is4xxClientError();
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
throw new RestTemplateException(IOUtils.toString(response.getBody(), StandardCharsets.UTF_8));
}
}
2
Apparently the issue was the usage of testRestTemplate, that for some reason being injected with handling errors by default. I used regular RestTemplate and indeed got the exception with the expected object