In my request handler, if the passed-in accountId cannot be converted to a valid ObjectId I want to catch the error and send back a meaningful message; however, doing so causes the return type to be incompatible, and I cannot figure out how to achieve this pretty trivial use case.
My code:
#GetMapping("/{accountId}")
public Mono<ResponseEntity<Account>> get(#PathVariable String accountId) {
log.debug(GETTING_DATA_FOR_ACCOUNT, accountId);
try {
ObjectId id = new ObjectId(accountId);
return repository.findById(id)
.map(ResponseEntity::ok)
.switchIfEmpty(Mono.just(ResponseEntity.notFound().build()));
} catch (IllegalArgumentException ex) {
log.error(MALFORMED_OBJECT_ID, accountId);
// TODO(marco): find a way to return the custom error message. This seems to be currently
// impossible with the Reactive API, as using body(message) changes the return type to
// be incompatible (and Mono<ResponseEntity<?>> does not seem to cut it).
return Mono.just(ResponseEntity.badRequest().build());
}
}
The body(T body) method changes the type of the returned Mono so that it is (assuming one just sends a String) a Mono<ResponseEntity<String>>; however, changing the method's return type to Mono<ResponseEntity<?>> does not work either:
...
return Mono.just(ResponseEntity.badRequest().body(
MALFORMED_OBJECT_ID.replace("{}", accountId)));
as it gives an "incompatible type" error on the other return statement:
error: incompatible types: Mono<ResponseEntity<Account>> cannot be converted to Mono<ResponseEntity<?>>
.switchIfEmpty(Mono.just(ResponseEntity.notFound().build()));
Obviously, changing the return type of the method to Mono<?> would work, but the response then is the serialized JSON of the ResponseEntity which is NOT what I want.
I have also tried using the onErrorXxxx() methods, but they do not work here either, as the conversion error happens even before the Flux is computed, and I just get a "vanilla" 400 error with an empty message.
The only way I can think of working around this would be to add a message field to my Account object and return that one, but it's genuinely a horrible hack.
#thomas-andolf's answer helped me figure out the actual solution.
For anyone stumbling upon this in future, here is how I actually solved the puzzle (and, yes, you still need the try/catch to intercept the error thrown by the ObjectId constructor):
#GetMapping("/{accountId}")
public Mono<ResponseEntity<Account>> get(#PathVariable String accountId) {
return Mono.just(accountId)
.map(acctId -> {
try {
return new ObjectId(accountId);
} catch (IllegalArgumentException ex) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
MALFORMED_OBJECT_ID));
}
})
.flatMap(repository::findById)
.map(ResponseEntity::ok)
.switchIfEmpty(Mono.just(ResponseEntity.notFound().build()));
}
To actually see the message in the returned body, you will need to add server.error.include-message=always in application.properties (see here).
Using onError() won't work here (I did try that, in all its variants) as it requires a Mono<ResponseEntity<Account>> and there is no way to generate one from the error status (when adding the message body).
Related
I am fairly new to Java and Spring Boot (coming from TypeScript) and
experimenting with a small restful CRUD Controller using the
reactive Spring Boot API.
There are many tutorials and examples out there but they all lack
proper response statuses, e.g. giving a 404 on DELETE when the
resource doesn't exist.
What I like to achieve is a DELETE handler which
returns "204 No Content" if the resource existed and was deleted successfully
returns "404 Not found" if the resource doesn't exist
A simple "I don't care about HTTP status" DELETE handler
looks like this:
#DeleteMapping("/{id}")
public Mono<Void> deletePet(#PathVariable String id) {
return petRepository.deleteById(id);
}
This always gives status 200, even when there is no Pet for this ID.
I tried to use petRepository.findById(id) and .defaultIfEmpty()
in several ways to catch the 404 case, but without luck. E.g. with
this implementation I am getting always 204:
#DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> deletePet(#PathVariable String id) {
return petRepository.findById(id)
.map(pet1 -> new ResponseEntity<Void>(HttpStatus.NO_CONTENT))
.defaultIfEmpty(new ResponseEntity<Void>(HttpStatus.NOT_FOUND))
.flatMap(res -> {
return petRepository.deleteById(id)
.map(v -> new ResponseEntity<Void>(HttpStatus.NO_CONTENT));
});
}
I think I understand why this isn't working, because after the .defaultIfEmpty()
the Mono isn't empty anymore and the .flatMap will have something to work
on (the 404 response) so the deleteById() is executed. This returns an (obviously)
non empty Mono as well, so the status turns into NO_CONTENT again.
But all my (many) attempts to change this failed so I hope anyone has the right
solution for this problem.
Thanks! :)
When findById returns an empty Mono, the code below will not executed either map or flatMap and will only return the value from defaultIfEmpty
#DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> deletePet(#PathVariable String id) {
return petRepository.findById(id)
.map(pet1 -> new ResponseEntity<Void>(HttpStatus.NO_CONTENT))
.flatMap(res -> {
return petRepository.deleteById(id)
.map(v -> new ResponseEntity<Void>(HttpStatus.NO_CONTENT));
})
.defaultIfEmpty(new ResponseEntity<Void>(HttpStatus.NOT_FOUND));
}
Also, your understanding as to why this happens in your code snippet is correct.
After some more research I found a solution:
#DeleteMapping("/{id}")
#ResponseStatus(HttpStatus.NO_CONTENT)
public Mono<ResponseEntity<Void>> deletePet(#PathVariable String id) {
return petRepository.findById(id)
.defaultIfEmpty(new Pet())
.flatMap(pet -> {
if (null == pet.id) {
return Mono.just(new ResponseEntity<Void>(HttpStatus.NOT_FOUND));
}
else {
return petRepository.deleteById(id)
.map(v -> new ResponseEntity<Void>(HttpStatus.NO_CONTENT));
}
});
}
Now an empty Pet object is created when findById() gives an empty
result using defaultIfEmpty().
So the flatMap() gets either the Pet for the given ID or an empty
Pet. The latter is recognized by the fact that the id property is null
which is turned into a 404 response. In the other case the Pet is
deleted and 204 is returned. But note, that the .map() there isn't executed because of the empty deleteById() result. It's just necessary to satisfy the generic interface here. The 204 comes from the #ResponseStatus annotation.
So this is a possible solution - but it looks not very elegant to me (creating an empty Pet and having this no-op deleteById().map()).
If there is a better way to do this, please give your answer here.
I have this catch statement:
catch (NotFoundException ex) {
ex.getError().setTitle(NOT_FOUND);
throw new NotFoundException(resource, id, ex.getError());
}
How can I mock this exception? I've tried this
when(service
.filter(eq(any()), eq(any()), eq(any())))
.thenThrow(new NotFoundException(anyString(), anyString()));`
But it gives me a null exception error because of this line:
ex.getError().setTitle(NOT_FOUND);
The constructor is:
public NotFoundException(String resource, String id, Error error) {
this.resource = resource;
this.ids = Collections.singletonList(id);
this.error = error;
}
And I can't get the exception variable to set the title, or find an way to mock it.
Thanks for you help!
.thenThrow(new NotFoundException(anyString(), anyString()));
This isn't allowed: anyString() only stands directly in for the call in when and verify. In your call to filter, simply use any() rather than eq(any()), but you're otherwise using matchers in the correct place.
Furthermore, it looks like your system-under-test assumes that ex.getError() is non-null; it is likely that you'll need to pass in a useful Error instance as constructor parameter into the NotFoundException you create.
.thenThrow(new NotFoundException("foo", "bar", new Error(/* ... */)))
Naturally, if your Error is difficult to create or work with, you might use a mock(Error.class) instead.
I can’t understand how to handle the following error:
In the class CustomerService I delete the customer by id, and if such an id does not exist, then an error must be thrown! How can you do without an if else construct?
CustomerService:
// Delete customer
public void deleteCustomer(Long id){
Customer customer = customerRepository.getByIdAndUserRole(id, "customer");
customerRepository.delete(customer);
}
CustomerController:
// DELETE MAPPING
//
// Delete customer with ID
#DeleteMapping("/customers/{id}")
void deleteCustomer(#PathVariable Long id) {
customerService.deleteCustomer(id);
}
Try to use Controller Advice. Whenever a exception occur it will directly handled by the handler. No if/else or try/catch blocks will be required.
1) Create a class CustomerControllerHandler, annotate with #ControllerAdvice.
2) Now create methods with arguments having the type of Exception.
3) The methods will return the JSON/POJO/void you want.
4) Annotate the methods with #ExceptionHandler(Exception.class) and
#ResponseStatus(HttpStatus.BAD_REQUEST),
#ControllerAdvice
public class CustomerControllerHandler {
#ExceptionHandler(Exception.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
public void processException(Exception ex) {
}
}
You can try using this instead. It's the deleteById method for a CrudRepository (hope you're using that) and it throws IllegalArgumentException if it can't find a customer.
I assumed that with "error" you meant "exception" and then in the controller you can surround with a try-catch block like that:
try{
customerService.deleteCustomer(id);
} catch (IllegalArgumentException e) {
log.error("No customer id exists!", e);
// if you have no logger, then use System.out.println() at least
}
If you wanted instead to return an error to the caller, then change the data type from void to HttpResponse<String> and when catching an exception you can return HttpResponse<>("No customer exists with that id!", HTTP.BAD_REQUEST). Now the caller will get a 400 - bad request.
A nicer approach would be to catch the exception in the service itself and return a boolean to the controller (true if customer is deleted and false if couldn't delete / couldn't find one).
if you want to throw error then you will have to check a condition, that is there will be an if statement, but not necessarily an else is needed.
For instance, you can check response of delete and throw error according to below one.
if (deleteCount == 0) {
//throw error here
}
I'm building a REST API with SpringBoot and decided to build it in SpringBoot last version.
The problem I am having, is that for some reason my code seems not to be reaching OrElseGet, or I'm not knowing how to deal with the Optional stuff.
What I want to do is return the status code 200 and the entity in case the object is found in the database, and status code 404 if not found.
However, when specifying an invalid code, I am getting the string null in the response body and the status code 200.
Here is my code:
#GetMapping("/{codigo}")
public ResponseEntity<Optional<Categoria>> searchByCode(#PathVariable Long codigo) {
return Optional
.ofNullable( categoriaRepository.findById(codigo) )
.map(cat-> ResponseEntity.ok().body(cat))
.orElseGet(() -> ResponseEntity.notFound().build());
}
Any help would be appreciated.
There are few improvements that can be done here. Firstly why are you returning an Optional from your REST Controller. I don't see any point there. You can merely return the Categoria object instead and let jackson to serialize it into a json payload. So change the code as below.
#RequestMapping(value = "/{codigo}")
public ResponseEntity<Categoria> searchByCode(#PathVariable Long codigo) {
return categoriaRepository.findById(codigo).map(cat -> ResponseEntity.ok().body(cat))
.orElse(ResponseEntity.notFound().build());
}
And here's the mock repository I used to simulate this.
public interface CategoriaRepository extends JpaRepository<Categoria, Integer>{
default Optional<Categoria> findById(long codigo) {
// return Optional.ofNullable(new Categoria(1, "name"));
return Optional.ofNullable(null);
}
}
Also wrapping the response inside of another Optional adds some complexity to your code while making it much harder to read too. This should give you the desired result.
This question already has answers here:
How to handle REST Exceptions?
(2 answers)
Closed 7 years ago.
I would like to throw Exceptions from my REST endpoints. However, I am not too familiar with good REST design techniques. Consider the following...
//note the throws clause
#POST
public Response saveNewActivity(#HeaderParam("sessionTokenString") String sessionTokenString, Activity activity) throws Exception {
Activity result = as.saveNewActivity(activity);
if (result == null) {
throw new DuplicateDataException("blah blah blah");
}
return Response.ok(result).build();
}
versus handling Exceptions and explicitly returning only responses
#POST
public Response saveNewActivity(#HeaderParam("sessionTokenString") String sessionTokenString, Activity activity) {
try {
Activity result = as.saveNewActivity(activity);
if (result == null) {
throw new DuplicateDataException("blah blah blah");
}
return Response.ok(result).build();
} catch (Exception e) {
return Response.status(Response.Status.SOME_STATUS).build();
}
}
I can have the DuplicateDataException mapped using ExceptionMapper as follows
public class DuplicateDataExceptionMapper implements ExceptionMapper<DuplicateDataException> {
#Override
public Response toResponse(DuplicateDataException e) {
ErrorMessage errorMessage = new ErrorMessage ("Activity names must be unique.", <HttpStatusNumber>, "<add documentation here>");
return Response.status(Status.NOT_FOUND).entity(errorMessage).build();
}
}
Although in the end a Response gets returned anyways, but is one way of handling Exceptions (whether or not they are RuntimeExceptions) preferred over another, or does it not really matter? I have never seen a throws statement on a REST endpoint which is why I ask.
Please note that
this question or this question did not give the answer I am looking for.
If a parent class is catching the thrown exception and returning an appropriate HTTP Reponse error code (4xx), the initial code is fine.
If there's no parent class catching these to make them a 4xx instead of a 500, your code - to change the response code to something appropriate for this specific error - seems like a really good idea.
It is a bad idea to through an exception.
From the client perspective it results in an http 500 error code that don't tell nothing to an automatic program.
You should design your code trying to intercept all possible errors and reply with an appropriate error code inside your valid response. If your response in a json answer with something like the following:
{
statusCode: 345,
errorMessage: 'The error code message'
}
Leave the http status code 500 to unexpected errors.