How do I stream multiple multipart messages of different mimetypes in spring? - spring

I'm currently using Springs ResponseBodyEmitter to stream a multipart/related response consisting of multiple parts (of mimetype application/json as well as application/octet-stream) to a client. Therefore I am manually setting the boundary in the Content-Type header as well as creating the encapsulation boundaries between the different message parts within the payload. I'm pretty sure there is a more convenient way to achieve this. What would be the idiomatic way in Spring to achieve this?
#GetMapping(value = "/data", produces = {MediaType.MULTIPART_FORM_DATA_VALUE})
public ResponseEntity<ResponseBodyEmitter> streamMultipart() {
// omitting actual contents for the sake of brevity
InputStream audio = new ByteArrayInputStream(null); //asynchronously retrieved
String json = "{}";
ResponseBodyEmitter speakEmitter = new ResponseBodyEmitter();
executor.execute(() -> {
try {
speakEmitter.send("\r\n--myBoundary\r\n");
speakEmitter.send("Content-Type: application/json;\r\n\r\n");
speakEmitter.send(json, MediaType.APPLICATION_JSON);
speakEmitter.send("\r\n--myBoundary\r\n");
speakEmitter.send("Content-Type: application/octet-stream;\r\n\r\n");
speakEmitter.send(audio.readAllBytes(), MediaType.APPLICATION_OCTET_STREAM);
speakEmitter.send("\r\n--myBoundary--\r\n");
} catch (IOException ignoredForBrevity) {}
});
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.add(HttpHeaders.CONTENT_TYPE, "multipart/related; boundary=myBoundary");
return ResponseEntity.ok().headers(responseHeaders).body(speakEmitter);
}

Related

How to set multiple headers at once in Spring WebClient?

I was trying to set headers to my rest client but every time I have to write
webclient.get().uri("blah-blah")
.header("key1", "value1")
.header("key2", "value2")...
How can I set all headers at the same time using headers() method?
If those headers change on a per request basis, you can use:
webClient.get().uri("/resource").headers(httpHeaders -> {
httpHeaders.setX("");
httpHeaders.setY("");
});
This doesn't save much typing; so for the headers that don't change from one request to another, you can set those as default headers while building the client:
WebClient webClient = WebClient.builder().defaultHeader("...", "...").build();
WebClient webClient = WebClient.builder().defaultHeaders(httpHeaders -> {
httpHeaders.setX("");
httpHeaders.setY("");
}).build();
The consumer is correct, though it's hard to visualize, esp. in that you can continue with additional fluent-composition method calls in the webclient construction, after you've done your work with the headers.
....suppose you have a HttpHeaders (or MutliValue map) holding your headers in scope. here's an example, using an exchange object from spring cloud gateway:
final HttpHeaders headersFromExchangeRequest = exchange.getRequest().headers();
webclient.get().uri("blah-blah")
.headers( httpHeadersOnWebClientBeingBuilt -> {
httpHeadersOnWebClientBeingBuilt.addAll( headersFromExchangeRequest );
}
)...
the addAll can take a multivalued map. if that makes sense. if not, let your IDE be your guide.
to make the consumer clearer, let's rewrite the above as follows:
private Consumer<HttpHeaders> getHttpHeadersFromExchange(ServerWebExchange exchange) {
return httpHeaders -> {
httpHeaders.addAll(exchange.getRequest().getHeaders());
};
}
.
.
.
webclient.get().uri("blah-blah")
.headers(getHttpHeadersFromExchange(exchange))
...
I found this problem came up again for me and this time I was writing groovy directly using WebClient. Again, the example I'm trying to drive is using the Consumer as the argument to the headers method call.
In groovy, the additional problem is that groovy closure syntax and java lambda syntax both use ->
The groovy version is here:
def mvmap = new LinkedMultiValueMap<>(headersAsMap)
def consumer = { it -> it.addAll(mvmap) } as Consumer<HttpHeaders>
WebClient client = WebClient.create(baseUrlAsString)
def resultAsMono = client.post()
.uri(uriAsString).accept(MediaType.APPLICATION_JSON)
.headers(consumer)
.body(Mono.just(payload), HashMap.class)
.retrieve()
.toEntity(HashMap.class)
The java version is here:
LinkedMultiValueMap mvmap = new LinkedMultiValueMap<>(headersAsMap);
Consumer<HttpHeaders> consumer = it -> it.addAll(mvmap);
WebClient client = WebClient.create(baseUrlAsString);
Mono<ResponseEntity<HashMap>> resultAsMono = client.post()
.uri(uriAsString).accept(MediaType.APPLICATION_JSON)
.headers(consumer)
.body(Mono.just(payload), HashMap.class)
.retrieve()
.toEntity(HashMap.class);
In Spring Boot 2.7.5:
webClient
.get()
.uri("blah-blah")
.headers(
httpHeaders -> {
httpHeaders.set("key1", "value1");
httpHeaders.set("key2", "value2");
})

How to mock a multipart file upload when using Spring and Apache File Upload

The project I'm working on needs to support large file uploads and know the time taken during their upload.
To handle the large files I'm using the streaming API of Apache FileUpload, this also allows me to measure the time taken for the complete stream to be saved.
The problem I'm having is that I cannot seem to be able to utilise MockMvc in an Integration Test on this controller. I know that the controller works as I've successfully uploaded files using postman.
Simplified Controller Code:
#PostMapping("/upload")
public String handleUpload(HttpServletRequest request) throws Exception {
ServletFileUpload upload = new ServletFileUpload();
FileItemIterator iterStream = upload.getItemIterator(request);
while (iterStream.hasNext()) {
FileItemStream item = iterStream.next();
String name = item.getFieldName();
InputStream stream = item.openStream();
if (!item.isFormField()) {
// Process the InputStream
} else {
String formFieldValue = Streams.asString(stream);
}
}
}
Simplified Test Code:
private fun uploadFile(tfr: TestFileContainer) {
val mockFile = MockMultipartFile("file", tfr.getData()) // .getData*() returns a ByteArray
val receiveFileRequest = MockMvcRequestBuilders.multipart("/upload")
.file(mockFile)
.contentType(MediaType.MULTIPART_FORM_DATA)
val result = mockMvc.perform(receiveFileRequest)
.andExpect(status().isCreated)
.andExpect(header().exists(LOCATION))
.andReturn(
}
This is the error I'm currently getting
org.apache.tomcat.util.http.fileupload.FileUploadException: the
request was rejected because no multipart boundary was found
Can anyone help?
The MockMultipartFile approach won't work as Spring does work behind the scenes and simply passes the file around.
Ended up using RestTemplate instead as it actually constructs requests.

Globally formatting .net Web Api response

I have a Web Api service that retrieves data from another service, which returns Json. I don't want to do anything to the response, I just want to return it directly to the client.
Since the response is a string, if I simply return the response, it contains escape characters and messy formatting. If I convert the response in to an object, the WebApi will use Json.Net to automatically format the response correctly.
public IHttpActionResult GetServices()
{
var data = _dataService.Get(); //retrieves data from a service
var result = JsonConvert.DeserializeObject(data); //convert to object
return Ok(result);
}
What I would like is to either A: Be able to return the exact string response from the service, without any of the escape characters and with the proper formatting, or B: Set a global settings that will automatically Deserialize the response so that the Web Api can handle it the way I am doing it already.
On Startup I am setting some values that describe how formatting should be handled, but apparently these aren't correct for what im trying to do.
HttpConfiguration configuration = new HttpConfiguration();
var settings = configuration.Formatters.JsonFormatter.SerializerSettings;
settings.Formatting = Formatting.Indented;
settings.ContractResolver = new DefaultContractResolver();
Do I need to create a custom ContractResolver or something? Is there one that already handles this for me?
Thanks
If you want to just pass through the json (Option A), you can do this
public IHttpActionResult GetServices() {
var json = _dataService.Get(); //retrieves data from a service
HttpContent content = new System.Net.Http.StringContent(json, Encoding.UTF8, "application/json");
var response = Request.CreateResponse(HttpStatusCode.OK);
response.Content = content;
return ResponseMessage(response);
}

WebApi Output Cache

I'm trying to implement an output cache in WebApi that can cache the responses already processed by filters and generate responses that are not processed by formatters.
From what I've seen ActionFilterAttribute's OnActionExecuting and OnActionExecuted are execute before the serialization formatters and so if you cache a response, at a cache hit you will respond the exact same content and that content will be serialized again to transmission.
As a possible solution in MVC I think that you can do this by implementing a IResultFilter that override OnResultExecuted by caching the serialized response. With this approach I don't know how to intercept the request handling to avoid the serialization formatters, I think that a possible solution to intercept is create a custom ActionResult to be handled directly by IResultFilter. Please note that this solution is not suitable for me because I'm implementing OutputCache in a WebApi application.
While writing the response, formatters in Web API come into action for HttpContents of type ObjectContent only.
In you OnActionExecuted method, you could force the serialization to happen by doing something like below and then set the response content as StreamContent (this way the formatters wouldn't be coming into picture):
An example below:
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
HttpResponseMessage response = actionExecutedContext.Response;
if (response != null && response.IsSuccessStatusCode)
{
ObjectContent originalContent = response.Content as ObjectContent;
if (originalContent != null)
{
MemoryStream ms = new MemoryStream();
// NOTE:
// 1. We are forcing serialization to occur into a buffered stream here
// 2. This can cause exception. You can leave it as it is and Web API's exception handling mechanism should
// do the right thing.
originalContent.CopyToAsync(ms).Wait();
// reset the position
ms.Position = 0;
StreamContent newContent = new StreamContent(ms);
// Copy the headers
foreach (KeyValuePair<string, IEnumerable<string>> header in originalContent.Headers)
{
newContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
//dispose the original content
originalContent.Dispose();
//TODO: cache this new httpcontent 'newContent' (so we are caching both serialized body + headers too)
//Set the response
//NOTE: This newContent will not hit the formatters
actionExecutedContext.ActionContext.Response.Content = newContent;
}
}
}

Attempting to test rest service with multipart file

I am attempting to test a rest service I created. The service is a post.
I wanted to create a file to pass the parameters(including a multi-part file).
From there I am trying to call the service at this point.
Pretty sure the service that doesn't work. But when I call rest Service. I have a simple form that just passes in a couple values including the jpg.
Here is the code.
HttpMessageConverter bufferedIamageHttpMessageConverter = new ByteArrayHttpMessageConverter();
restTemplate.postForObject("http://localhost:8080/sendScreeenAsPostCard", uploadItem.getFileData(), String.class));
My method signature is:
ResultStatus sendScreenAsPostcard( #RequestParam MultipartFile image, #RequestParamString userId)
That is the error I am getting.
Could not write request: no suitable HttpMessageConverter found for request type [org.springframework.web.multipart.commons.CommonsMultipartFile]
You need to simulate a file upload, which requires a particular content type header, body parameters, etc. Something like this should do the trick:
// Fill out the "form"...
MultiValueMap<String, Object> parameters = new LinkedMultiValueMap<String, Object>();
parameters.add("file", new FileSystemResource("file.jpg")); // load file into parameter
parameters.add("blah", blah); // some other form field
// Set the headers...
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "multipart/form-data"); // we are sending a form
headers.set("Accept", "text/plain"); // looks like you want a string back
// Fire!
String result = restTemplate.exchange(
"http://localhost:8080/sendScreeenAsPostCard",
HttpMethod.POST,
new HttpEntity<MultiValueMap<String, Object>>(parameters, headers),
String.class
).getBody();

Resources