How to combine Flux and ResponseEntity in Spring Webflux controllers - java

I use Monos with ResponseEntitys in my Webflux controllers in order to manipulate headers and other response info. For example:
#GetMapping("/{userId}")
fun getOneUser(#PathVariable userId: UserId): Mono<ResponseEntity<UserDto>> {
return repository.findById(userId)
.map(User::asDto)
.map { ResponseEntity.ok(it) }
.defaultIfEmpty(ResponseEntity.notFound().build())
}
#GetMapping
fun getAllUsers(): Flux<UserDto> {
return repository.findAllActive().map(User::asDto)
}
both works fine but there are cases where it is required to have ResponseEntity in conjunction with Flux as well. What should the response type be? Is it correct to use ResponseEntity<Flux<T>>?
For example:
#GetMapping("/{userId}/options")
fun getAllUserOptions(#PathVariable userId: UserId): ??? {
return repository.findById(userId)
.flatMapIterable{ it.options }
.map { OptionDto.from(it) }
// if findById -> empty Mono then:
// return ResponseEntity.notFound().build() ?
// else:
// return the result of `.map { OptionDto.from(it) }` ?
}
The behaviour I'd like to achieve here is that getAllUserOptions returns 404 if repository.findById(userId) is an empty Mono, otherwise return user.options as Flux.
Update:
repository here is ReactiveCrudRepository

Use switchIfEmpty to throw an exception in case the user doesn't exist:
return repository
.findById(userId)
.switchIfEmpty(Mono.error(NotFoundException("User not found")))
.flatMapIterable{ it.options }
.map { OptionDto.from(it) }
Then with an exception handler translate it to a 404 response.

You can use by returning Mono with ResponseEntity
like this
public Mono<ResponseEntity<?>> oneRawImage(
#PathVariable String filename) {
// tag::try-catch[]
return imageService.findOneImage(filename)
.map(resource -> {
try {
return ResponseEntity.ok()
.contentLength(resource.contentLength())
.body(new InputStreamResource(
resource.getInputStream()));
} catch (IOException e) {
return ResponseEntity.badRequest()
.body("Couldn't find " + filename +
" => " + e.getMessage());
}
});
}
I have also example like this
public ResponseEntity<Mono<?>> newLive(#Valid #RequestBody Life life) {
Mono<Life> savedLive = liveRepository.save(life);
if (savedLive != null) {
return new ResponseEntity<>(savedLive, HttpStatus.CREATED);
}
return new ResponseEntity<>(Mono.just(new Life()), HttpStatus.I_AM_A_TEAPOT);
}
I dislike functional programming in the REST controllers.
Here is an example ReactiveController .

works for me, let me know if you have a trouble
#PostMapping(value = "/bizagi/sendmsg")
public Mono<ResponseEntity<?>> sendMessageToQueue(#RequestBody BizagiPost bizagiPost) {
Mono<BodyReturn> retorno = useCase.saveMsg(bizagiPost);
Map<String, Object> response = new HashMap<>();
return retorno.map(t ->
{
if (t.getStatusCode().equals("200")) {
response.put("message", t.getReazon());
return new ResponseEntity(t, HttpStatus.OK);
} else {
response.put("message", t.getReazon());
return new ResponseEntity(t, HttpStatus.BAD_REQUEST);
}
});
}

Related

Handle errors in WebClient with and without body

I have implemented the error handling in a filter that looks like this:
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
URI url = request.url();
HttpMethod method = request.method();
return next.exchange(request).flatMap(response -> {
if (response.statusCode().isError()) {
return response.bodyToMono(String.class).flatMap(responseBody -> {
Optional<Exception> exception = errorResponseHandler.handleError(method, response.statusCode(), url, responseBody);
if (exception.isPresent()) {
return Mono.error(exception.get());
} else {
// fallback
return Mono.error(new UnsupportedOperationException("The fallback functionality is still missing"));
}
});
} else {
return Mono.just(response);
}
});
}
This should work fine in the case where the response comes with a body as then the response.bodyToMono(String.class).flatMap(...) is executed. However when the body is empty nothing happens, but what I want is to also deal with the error. It is my understanding that I would do this something like this;
response.bodyToMono(String.class).flatMap(...)
.switchIfEmpty(Mono.error(new UnsupportedOperationException("The body was empty")));
This does not work as the expected type to be returned is Mono instead of Mono.
How can I achieve the handling of errors with and without response body, which is needed to construct to correct exception?
This question brought me onto the right track:
The switchIfEmpty invocation has to come before the flatMap. As there is no body, flatMap is not executed and neither is anything after, therefore the switchIfEmpty has to come first:
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
URI url = request.url();
HttpMethod method = request.method();
return next.exchange(request).flatMap(response -> {
if (response.statusCode().isError()) {
return response.bodyToMono(String.class)
.switchIfEmpty(Mono.error(new UnsupportedOperationException("The body was empty")));
.flatMap(responseBody -> {
Optional<Exception> exception = errorResponseHandler.handleError(method, response.statusCode(), url, responseBody);
if (exception.isPresent()) {
return Mono.error(exception.get());
} else {
// fallback
return Mono.error(new UnsupportedOperationException("The fallback functionality is still missing"));
}
});
} else {
return Mono.just(response);
}
});
}

Grails RestBuilder dont findendPoint with Object in signature

I have a code with a RestBuilder that needs to connect to another application, the target endPoint have an object in the signature with the attributes. The problem is the request return 404. How I solve this? I tried use x-www-form-urlencoded (doesn't work)
Request Method:
RestResponse restResponse;
String parameters = '{"qtdThreads":3,"channel":"http://localhost:8081/application2"}'
try {
restResponse = new RestBuilder().post("http://localhost:8080/application/endPoint", {
accept("application/json")
contentType "application/json; utf-8"
body(parameters.getBytes("UTF-8"))
connectTimeout: 1000
})
} catch (Exception e) {
e.printStackTrace();
} finally {
return restResponse;
}
Target endPoint:
Object endPoint(ObjectCommand command) {
render(status: HttpStatus.OK)
}
Object used on signature
import grails.validation.Validateable
#Validateable
class ObjectCommand {
URL channel
Integer qtdThreads
static constraints = {
qtdThreads(validator: { Integer val ->
if (!val || val <= 0) {
return "message1"
}
})
channel(validator: { URL val ->
if (!val) {
return "message2"
}
})
}
}
did you check if the target application is running and exposing that endpoint?

Making HTTP requests from the post filter of Spring cloud gateway

I am new to reactive programming in Java and got stuck on this. I have implemented a filter that is working fine till the pre-filter phase but we want to make HTTP requests to other services in the post-filter.
My filter looks like this:
#Override
public GatewayFilter apply (AuthFilter.AuthenticationFilterConfigBean config) {
return (exchange, chain) -> {
ServerHttpRequest originalRequest = exchange.getRequest();
HttpHeaders requestHeaders = originalRequest.getHeaders();
WebClient.RequestHeadersSpec requestAuthentication = webClientAuthentication.post()
.uri("/xyzAuth")
.header("Header key","value")
.bodyValue("req body...");
Mono<ClientResponse> responseAuthentication = requestAuthentication.exchange();
return responseAuthentication.flatMap(clientResponseAuthentication -> {
return clientResponseAuthentication.bodyToMono(String.class).flatMap(bodyAuthentication -> {
if (!clientResponseAuthentication.statusCode().equals(HttpStatus.OK)) {
return this.handleExceptionCases(exchange, bodyAuthentication, clientResponseAuthentication.statusCode());
}
final AuthenticationResponse authenticationResponse;
try {
authenticationResponse = objectMapper.readValue(bodyAuthentication, AuthenticationResponse.class);
exchange.getRequest().mutate().headers(headersFromAuthService -> {
headersFromAuthService.addAll(getHeadermap(authenticationResponse.getHeaderInfo()));
}).build();
} catch (JsonProcessingException e) {
LOGGER.error("Failed parsing Authentication Response: " + e.getMessage());
return handleExceptionCases(exchange, null, HttpStatus.INTERNAL_SERVER_ERROR);
}
LOGGER.info("Prefilter Completed");
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
LOGGER.info("Postfilter Begins");
ServerHttpResponse response = exchange.getResponse();
if(response.getStatusCode().is2xxSuccessful()) {
WebClient.RequestHeadersSpec requestPostHandler = webClientPostHandler.post()
.uri("/xyzPostHandler")
.header("Header key","value")
.bodyValue("req body....");
Mono<ClientResponse> responsePostHandler = requestPostHandler.exchange();
LOGGER.info("Going to call postHandler");
return responsePostHandler.flatMap(postHandlerResponse -> {
return postHandlerResponse.bodyToMono(void.class).flatMap(postHandlerBody -> {
if (!postHandlerBody.statusCode().equals(HttpStatus.OK)) {
return this.handleExceptionCases(exchange, postHandlerBody.toString(), postHandlerResponse.statusCode());
}
});
});
}
}));
});
});
};
}
The pre filters are working fine (both the Authentication request and PreHandler requests are getting made) and the actual API is also getting hit. The problem arises when I am trying this in the post filter.
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
LOGGER.info("Postfilter Begins");
ServerHttpResponse response = exchange.getResponse();
if(response.getStatusCode().is2xxSuccessful()) {
WebClient.RequestHeadersSpec requestPostHandler = webClientPostHandler.post()
.uri("/xyzPostHandler")
.header("Header key","value")
.bodyValue("req body....");
Mono<ClientResponse> responsePostHandler = requestPostHandler.exchange();
LOGGER.info("Going to call postHandler");
return responsePostHandler.flatMap(postHandlerResponse -> {
return postHandlerResponse.bodyToMono(void.class).flatMap(postHandlerBody -> {
if (!postHandlerBody.statusCode().equals(HttpStatus.OK)) {
return this.handleExceptionCases(exchange, postHandlerBody.toString(), postHandlerResponse.statusCode());
}
});
});
}}));
Eclipse is showing warning Void methods cannot return a value.
Is this the correct way to make HTTP requests in the post filter? And what am I doing wrong?

Send received spring FilePart without saving

I need to send FilePart received in RestController to API using WebClient,
how can I do this?
Found an example, which saves image to disk.
private static String UPLOAD_ROOT = "C:\\pics\\";
public Mono<Void> checkInTest(#RequestPart("photo") Flux<FilePart> photoParts,
#RequestPart("data") CheckInParams params, Principal principal) {
return saveFileToDisk(photoParts);
}
private Mono<Void> saveFileToDisk(Flux<FilePart> parts) {
return parts
.log("createImage-files")
.flatMap(file -> {
Mono<Void> copyFile = Mono.just(Paths.get(UPLOAD_ROOT, file.filename()).toFile())
.log("createImage-picktarget")
.map(destFile -> {
try {
destFile.createNewFile();
return destFile;
} catch (IOException e) {
throw new RuntimeException(e);
}
})
.log("createImage-newfile")
.flatMap(file::transferTo)
.log("createImage-copy");
return Mono.when(copyFile)
.log("createImage-when");
})
.log("createImage-flatMap")
.then()
.log("createImage-done");
}
Then read it again and send to anoter server
.map(destFile -> {
MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
try {
map.set("multipartFile", new ByteArrayResource(FileUtils.readFileToByteArray(destFile)));
} catch (IOException ignored) {
}
map.set("fileName", "test.txt");
WebClient client = WebClient.builder().baseUrl("http://localhost:8080").build();
return client.post()
.uri("/upload")
.contentType(MediaType.MULTIPART_FORM_DATA)
.syncBody(map)
.exchange(); //todo handle errors???
}).then()
Is there way to avoid saving file?
I will mention solution by #Abhinaba Chakraborty
provided in https://stackoverflow.com/a/62745370/4551411
Probably something like this:
#PostMapping(value = "/images/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Mono<ResponseEntity<Void>> uploadImages(#RequestPart("files") Flux<FilePart> fileParts) {
return fileParts
.flatMap(filePart -> {
return webClient.post()
.uri("/someOtherService")
.body(BodyInserters.fromPublisher(filePart.content(), DataBuffer.class))
.exchange()
.flatMap(clientResponse -> {
//some logging
return Mono.empty();
});
})
.collectList()
.flatMap(response -> Mono.just(ResponseEntity.accepted().build()));
}
This accepts MULTIPART FORM DATA where you can attach multiple image files and upload them to another service.

How to change response statuc code when using CompletableFuture.exceptionaly() in Spring?

I have a simple controller:
#RestController
public class SimpleController() {
public String get() {
if (System.nanoTime() % 2 == 0)
throw new IllegalArgumentException("oops");
return "ok"
}
}
Controller can throw simple exception, so i wrote controller advisor for it handling:
#ExceptionHandler(IllegalArgumentException.class)
#ResponseBody
public ResponseEntity<String> rejection(Rejection ex) {
return new ResponseEntity<>("bad", HttpStatus.CONFLICT);
}
Now i want to make get method async. But i don't know the best way for handling exception.
I tried:
public CompletableFuture<String> get() {
CompletableFuture.supplyAsync(
() -> {
if (System.nanoTime() % 2 == 0)
throw new IllegalArgumentException("oops");
return "ok";
}).exceptionally(thr -> {
//what should i do?
if (thr instanceof IllegalArgumentException)
throw ((IllegalArgumentException) t);
if (thr.getCause() instanceof IllegalArgumentException)
throw ((IllegalArgumentException) t.getCause());
return null;
}
}
But controller advisor still does not catch the exception.
Also i tried to return ResponseEntity("message", HttpStatuc.CONFLICT); in exceptionally block.
But in tests i still have MvcResult.getResponse().getStatus() == 200.
Any other idea?
Maybe it's a wrong way at all?
UPDATE
I don't know why, but it don't catch exceptions:
#Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncUncaughtExceptionHandler() {
#Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
System.out.println();
}
};
And even if it work, how to set http status to response?
Try to return a ResponseEntity instance:
return new ResponseEntity<>("bad", HttpStatus.CONFLICT);
out of exceptionally method.
Sorry guys, the problem not in code.
I just wrote bad tests.
Here the link for explaining:
https://sdqali.in/blog/2015/11/24/testing-async-responses-using-mockmvc/
Just use:
MvcResult result = mockMvc.perform(...).andReturn();
result = mockMvc.perform(asyncDispatch(result)).andReturn();
before you check the status or result response.

Categories