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.
Related
I am trying to update the token when I get a response with 401 status code.
In order to do that, I used web client. I know that this mainly used to do reactive development but since resttemplate will soon be deprecated I went for this option.
The issue I am facing is that when it does call the api endpoint to get the new token, it throws a 'java.lang.IllegalStateException: block()/blockFirst()/blockLast() '. And make sense as it stated in the exception message It is not supported in thread reactor-http-nio-3.
I saw that there is a map and flatmap option, but I couldn't figure out how to use it inside the doBeforeRetry() to make it process in a different stream.
I need to have that new token before retrying.
So the question is : How can I get the token via another call and then still do the retry ?
I was able to make it work by using a try catch but I would like to find the solution how to use it inside that retry method.
I also try to block the token request by replacing the token response by a Mono and block it by using myMono.toFuture().get() as stated here block()/blockFirst()/blockLast() are blocking error when calling bodyToMono AFTER exchange()
Here is the code :
Method responsible for the call :
public String getValueFromApi(HashMap<String, Object> filter) {
String response = "";
response = webclient
.post()
.uri(endpoint)
.header("token", token.getToken())
.bodyValue(filter)
.retrieve()
.bodyToMono(String.class)
.retryWhen(Retry.max(3).doBeforeRetry(
retrySignal -> tokenService.getTokenFromApi(env)
).filter(InvalidTokenException.class::isInstance))
.block();
return response;
}
Method that retrieve the token :
public void getTokenFromApi(Environment env) {
HashMap<String, String> requestBody = new HashMap<>();
requestBody.put("name", "name");
requestBody.put("password", "password");
String response = WebClient
.builder()
.baseUrl(BASE_PATH)
.defaultHeader(HttpHeaders.CONTENT_TYPE, "application/json")
.build()
.post()
.uri(tokenUri)
.body(BodyInserters.fromValue(requestBody))
.retrieve()
.bodyToMono(String.class)
.block();
getTokenFromResponse(response);
}
private void getTokenFromResponse(String reponse) {
JsonObject tokenObject = new Gson().fromJson(reponse, JsonObject.class);
setToken(tokenObject.get("token").getAsString());
}
WebClient Builder :
#Bean
public WebClient webClientForApi(WebClient.Builder webClientBuilder) {
return webClientBuilder
.clientConnector(new ReactorClientHttpConnector(httpClient))
.filter(errorHandler())
.filter(logRequest())
.clone()
.baseUrl(BASE_PATH)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, "application/json")
.build();
}
public ExchangeFilterFunction errorHandler() {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
if (clientResponse.statusCode().equals(HttpStatus.UNAUTHORIZED)) {
return Mono.error(InvalidTokenException::new);
} else if (clientResponse.statusCode() == HttpStatus.INTERNAL_SERVER_ERROR) {
return Mono.error(ApiInternalServerException::new);
} else {
return Mono.just(clientResponse);
}
});
}
private ExchangeFilterFunction logRequest() {
return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
logger.info("Request: {} {}", clientRequest.method(), clientRequest.url());
clientRequest.headers().forEach((name, values) -> values.forEach(value -> logger.info("{}={}", name, value)));
return Mono.just(clientRequest);
});
}
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?
I want ot implement WebFlux example client which can make request with http params and get the response body and http response code. I tried this:
public ClientResponse execute(NotificationMessage nm)
Mono<String> transactionMono = Mono.just(convertedString);
return client.post().uri(builder -> builder.build())
.header(HttpHeaders.USER_AGENT, "agent")
.body(transactionMono, String.class).exchange().block();
}
private static String convert(Map<String, String> map) throws UnsupportedEncodingException {
String result = map.entrySet().stream().map(e -> encode(e.getKey()) + "=" + encode(e.getValue()))
.collect(Collectors.joining("&"));
return result;
}
private static String encode(String s) {
try {
return URLEncoder.encode(s, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}
Can you give me some advice after .exchange() how I can get the http status code and all available body.
From the ClientResponse object returned by exchange you can use response.statusCode() to get the status and use response.bodyToMono() or bodyToFlux() to get the actual body. You should avoid using .block() in reactive programming and use .subscribe() or .flatMap() or other operators to get the data from Mono or Flux objects. Read more about reactive programming and Project reactor (used by spring webflux) here.
For eg:
public Mono<Data> execute(NotificationMessage nm)
return client.post().uri(builder -> builder.build())
.header(HttpHeaders.USER_AGENT, "agent")
.body(transactionMono, String.class).exchange()
.flatMap(response -> {
HttpStatus code = response.statusCode();
Data data = response.bodyToMono(Data.class);
return data;
});
}
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);
}
});
}
I have the following endpoint code to serve PDF files.
#RequestMapping
ResponseEntity<byte[]> getPDF() {
File file = ...;
byte[] contents = null;
try {
try (FileInputStream fis = new FileInputStream(file)) {
contents = new byte[(int) file.length()];
fis.read(contents);
}
} catch(Exception e) {
// error handling
}
HttpHeaders headers = new HttpHeaders();
headers.setContentDispositionFormData(file.getName(), file.getName());
headeres.setCacheControl("must-revalidate, post-check=0, pre-check=0");
return new ResponseEntity<>(contents, headers, HttpStatus.OK);
}
How can I convert above into a reactive type Flux/Mono and DataBuffer.
I have check DataBufferUtils but It doesn't seem to offer what I needed. I didn't find any example either.
The easiest way to achieve that would be with a Resource.
#GetMapping(path = "/pdf", produces = "application/pdf")
ResponseEntity<Resource> getPDF() {
Resource pdfFile = ...;
HttpHeaders headers = new HttpHeaders();
headers.setContentDispositionFormData(file.getName(), file.getName());
return ResponseEntity
.ok().cacheControl(CacheControl.noCache())
.headers(headers).body(resource);
}
Note that DataBufferUtils has some useful methods there that convert an InputStream to a Flux<DataBuffer>, like DataBufferUtils#read(). But dealing with a Resource is still superior.
Below is the code to return the attachment as byte stream:
#GetMapping(
path = "api/v1/attachment",
produces = APPLICATION_OCTET_STREAM_VALUE
)
public Mono<byte[]> getAttachment(String url) {
return rest.get()
.uri(url)
.exchange()
.flatMap(response -> response.toEntity(byte[].class));
}
This approach is very simple but the disadvantage is it will the load the entire attachment into memory. If the file size is larger, then it will be a problem.
To overcome we can use DataBuffer which will send the data in chunks. This is an efficient solution and it will work for any large size file. Below is the modified code using DataBuffer:
#GetMapping(
path = "api/v1/attachment",
produces = APPLICATION_OCTET_STREAM_VALUE
)
public Flux<DataBuffer> getAttachment(String url) {
return rest.get()
.uri(url)
.exchange()
.flatMapMany(response -> response.toEntity(DataBuffer.class));
}
In this way, we can send attachments in a reactive fashion.
Same Problem with me.
I use Webflux Spring WebClient
I write style RouterFunction
My solution below,
ETaxServiceClient.java
final WebClient defaultWebClient;
public Mono<byte[]> eTaxPdf(String id) {
return defaultWebClient
.get()
.uri("-- URL PDF File --")
.accept(MediaType.APPLICATION_OCTET_STREAM)
.exchange()
.log("eTaxPdf -> call other service")
.flatMap(response -> response.toEntity(byte[].class))
.flatMap(responseEntity -> Mono.just(Objects.requireNonNull(responseEntity.getBody())));
}
ETaxHandle.java
#NotNull
public Mono<ServerResponse> eTaxPdf(ServerRequest sr) {
Consumer<HttpHeaders> headers = httpHeaders -> {
httpHeaders.setCacheControl(CacheControl.noCache());
httpHeaders.setContentDisposition(
ContentDisposition.builder("inline")
.filename(sr.pathVariable("id") + ".pdf")
.build()
);
};
return successPDF(eTaxServiceClient
.eTaxPdf(sr.pathVariable("id"))
.switchIfEmpty(Mono.empty()), headers);
}
ETaxRouter.java
#Bean
public RouterFunction<ServerResponse> routerFunctionV1(ETaxHandle handler) {
return route()
.path("/api/v1/e-tax-invoices", builder -> builder
.GET("/{id}", handler::eTaxPdf)
)
.build();
}
CommonHandler.java
Mono<ServerResponse> successPDF(Mono<?> mono, Consumer<HttpHeaders> headers) {
return ServerResponse.ok()
.headers(headers)
.contentType(APPLICATION_PDF)
.body(mono.map(m -> m)
.subscribeOn(Schedulers.elastic()), byte[].class);
}
Result: Successfully displayed on the browser.
Work for me.