I'm trying to figure out how to write a method to simply send a file from a webflux controller to a 'regular' controller.
I'm constantly getting a common error back, but nothing I've tried has resolved it.
The method I'm sending the file from:
#GetMapping("process")
public Flux<String> process() throws MalformedURLException {
final UrlResource resource = new UrlResource("file:/tmp/document.pdf");
MultiValueMap<String, UrlResource> data = new LinkedMultiValueMap<>();
data.add("file", resource);
return webClient.post()
.uri(LAMBDA_ENDPOINT)
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(data))
.exchange()
.flatMap(response -> response.bodyToMono(String.class))
.flux();
}
I'm consuming it in a AWS Lambda with the following endpoint:
#PostMapping(path = "/input", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<List<?>> input(#RequestParam("file") MultipartFile file) throws IOException {
final ByteBuffer byteBuffer = ByteBuffer.wrap(file.getBytes());
//[..]
return new ResponseEntity<>(result, HttpStatus.OK);
}
But I'm constantly just getting:
{
"timestamp":1549395273838,
"status":400,
"error":"Bad Request",
"message":"Required request part 'file' is not present",
"path":"/detect-face"
}
back from the lambda;
Have I just setup the sending of the file incorrectly, Or do I need to configure something on the API Gateway to allow the request parameters in?
This was a interesting one for me. As I'm using a lambda function on the receiving end, and making use of aws-serverless-java-container-spring I actually had to declare the MultipartResolver manually.
The code in my question worked correctly once I added
#Bean
public MultipartResolver multipartResolver() {
return new CommonsMultipartResolver();
}
to my configuration.
Maybe someone will stumble on this and find it useful.
Related
I need to call an API from my microservice which returns binary file data which i have to return back to the requester system
I tried using below code:
#RequestMapping(value="/getDocument", method=RequestMethod.POST)
private ResponseEntity<byte[]> receiveFile(){
MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
map.add("mobile","XXXXXXXXX9");
WebClient webClient = WebClient.create();
ResponseEntity<byte[]> apiResponse = webClient.post()
.uri(new URI("https://api.myapp.net.in/getDocument"))
.header("username", "xyz")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
//.accept(MediaType.parseMediaType("application/pdf"))
.body(BodyInserters.fromFormData(map))
.retrieve()
.toEntity(byte[].class)
.block();
return apiResponse;
}
When i execute the above code and try to consume from Postman i am getting error as unable to open file but when i directly call the https://api.myapp.net.in/getDocument API via Postman i am able to download the pdf file properly.
Please let me know where am i going wrong.
I am trying to use WebClient to download a file from a external service and return it to the client. In the Rest Controller, I have the following endpoint:
#GetMapping(value = "/attachment/{fileId}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public Flux<byte[]> get(#PathVariable String fileId) {
return this.webClient.get()
.uri(builder ->
builder.scheme("https")
.host("external-service.com")
.path("/{fileId}")
.build(fileId)
).attributes(clientRegistrationId("credentials"))
.accept(MediaType.APPLICATION_OCTET_STREAM)
.retrieve()
.bodyToFlux(byte[].class);
}
When I try to hit the endpoint, I get the following error:
Exception Class:class org.springframework.http.converter.HttpMessageNotWritableException
Stack Trace:org.springframework.http.converter.HttpMessageNotWritableException: No converter for [class org.springframework.web.servlet.mvc.method.annotation.ReactiveTypeHandler$CollectedValuesList] with preset Content-Type 'null'
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:317)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:181)
I have tried returning Flux<DataBuffer> instead, but getting the same error message.
I'm using spring-boot-starter-web and spring-boot-starter-webflux version 2.2.4.RELEASE. It is running on a tomcat server.
What am I missing?
Edit:
I'm trying to stream the result to the client without buffering the whole file into memory.
This works for me. Just change from Flux to Mono. Seem there is no converter for Flux<byte[]>
#GetMapping(value = "/attachment/{fileId}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public Mono<byte[]> get(#PathVariable String fileId) {
return this.webClient.get()
.uri(builder ->
builder.scheme("https")
.host("external-service.com")
.path("/{fileId}")
.build(fileId)
).attributes(clientRegistrationId("credentials"))
.accept(MediaType.APPLICATION_OCTET_STREAM)
.retrieve()
.bodyToMono(byte[].class);
}
The UI for my webapp has the ability to either upload a file(csv), or send the data as json in request body. However either a file upload, or a json request would be present in the request and not both. I am creating a spring rest controller which combine file upload and also accepts the request json values as well.
With the below endpoint tested from postman, I am not getting exception:
org.apache.tomcat.util.http.fileupload.FileUploadException: the request was rejected because no multipart boundary was found
#RestController
public class MovieController {
private static final Logger LOGGER = LoggerFactory.getLogger(MovieController.class);
#PostMapping(value="/movies", consumes = {"multipart/form-data", "application/json"})
public void postMovies( #RequestPart String movieJson, #RequestPart(value = "moviesFile") MultipartFile movieFile ) {
// One of the below value should be present and other be null
LOGGER.info("Movies Json Body {}", movieJson);
LOGGER.info("Movies File Upload {}", movieFile);
}
}
Appreciate any help in getting this issue solved?
Note: I was able to build two separate endpoint for file upload and json request, but that won't suffice my requirement. Hence I'm looking for a solution to combine both
Try something like:
#RequestMapping(value = "/movies", method = RequestMethod.POST, consumes = { "multipart/form-data", "application/json" })
public void postMovies(
#RequestParam(value = "moviesFile", required = false) MultipartFile file,
UploadRequestBody request) {
In RequestBody you can add the parameters you want to send.
This will not send the data as JSON.
Edit:- I forgot to add the variable for the Multipart file and I mistakenly used the RequestBody which is reserved keyword in spring.
Hope it helps.
I would suggest to create two separate endpoints. This splits and isolates the different functionality and reduces the complexity of your code. In addition testing would be easier and provides better readability.
Your client actually has to know which variable to use. So just choose different endpoints for your request instead of using different variables for the same endpoint.
#PostMapping(value="/movies-file-upload", consumes = {"multipart/form-data"})
public void postMoviesFile(#RequestPart(value = "moviesFile") MultipartFile movieFile ) {
LOGGER.info("Movies File Upload {}", movieFile);
}
#PostMapping(value="/movies-upload", consumes = {"application/json"})
public void postMoviesJson( #RequestPart String movieJson) {
LOGGER.info("Movies Json Body {}", movieJson);
}
I have a Spring app acting as a passthrough from one app to another, making a http request and returning a result to the caller using WebClient. If the client http call returns a 500 internal server error I want to be able to catch this error and update the object that gets returned rather than re-throwing the error or blowing up the app.
This is my Client:
final TcpClient tcpClient = TcpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeout)
.doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(readTimeout))
.addHandlerLast(new WriteTimeoutHandler(writeTimeout)));
this.webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient)))
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.filter(logRequest())
.filter(logResponse())
.filter(errorHandler())
.build();
And this is my error handler. I've commented where I want to modify the result, where ResponseDto is the custom object that is returned from the client call happy path
public static ExchangeFilterFunction errorHandler(){
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
ResponseDto resp = new ResponseDto();
if(nonNull(clientResponse.statusCode()) && clientResponse.statusCode().is5xxServerError()){
//this is where I want to modify the response
resp.setError("This is the error");
}
//not necessarily the correct return type here
return Mono.just(clientResponse);
});
}
How can I achieve this? I can't find any tutorials or any information in the docs to help explain it.
Disclaimer, I'm new to webflux. We're only starting to look at reactive programming
I need to create a rest service in java which will in turn connect to another rest service for file download. For now, I just need to transfer the file from the other backend to client but in future some processing/transformations would be done.
For all the web services in my project, we are using spring rest (for providing as well as consuming the services).
My question is what would be the appropriate way of doing it considering that the files would be large and I don't want to run into OutOfMemory errors.
People in some other posts have suggested to use streams on both the ends but is that really possible? For this, do I need to write the file on disk first?
My current code for file download (consumer) -
public BackendResponse<byte[]> callBackendForFile(BackendRequest request) {
String body = null;
ResponseEntity<byte[]> responseEntity = null;
URI uri = createURI(request);
MultiValueMap<String, String> requestHeaders = getHeadersInfo(request.getHttpRequest());
if (HttpMethod.GET.equals(request.getMethod())) {
responseEntity = restTemplate.exchange(uri, request.getMethod(),
new HttpEntity<String>(body, requestHeaders), byte[].class);
} else {
LOG.error("Method:{} not supported yet", request.getMethod());
}
BackendResponse<byte[]> response = new BackendResponse<>();
response.setResponse(responseEntity);
return response;
}
My client code (provider):
#RequestMapping(value = "/file", method = RequestMethod.GET, produces = "application/xml")
#ResponseBody
public void downloadFileWithoutSpring(HttpMethod method, HttpServletRequest httpRequest,
HttpServletResponse httpResponse) {
BackendRequest request = new BackendRequest(method,
httpRequest.getRequestURI(), httpRequest.getQueryString(), httpRequest);
BackendResponse<byte[]> backendResponse = dutyplanService.getFile(request);
ResponseEntity<byte[]> response = backendResponse.getResponse();
httpResponse.addHeader("Content-Disposition", "attachment; filename=\"" + "attachment.zip" + "\"");
httpResponse.getOutputStream().write(response.getBody());
httpResponse.flushBuffer();
}
Note: The code above doesn't work somehow as the attachment downloaded is a corrupt file
I don't think you will need to create that file on server as long as you are having the bytearray content of it received from another server.
You can try changing value of produces annotation to the value application/zip (or application/octet-stream, depending on the target browser) instead of 'application/xml'
you can pass HttpServletResponse#getOutputStream() directly in restTemplate and write it without save file in server.
public void getFile(HttpServletResponse response) throws IOException {
restTemplate.execute(
"http://ip:port/temp.csv",
HttpMethod.GET,
null,
clientHttpResponse -> {
StreamUtils.copy(clientHttpResponse.getBody(), response.getOutputStream());
return null;
}
);
}
note that after call getFile(), you should close outputStream like this
response.getOutputStream().close()