How to tell resteasy-reactive client that a #PathParam is already encoded? - quarkus

I am using quarkus resteasy-reactive and setting up clients declaratively.
I need to put base64 (which works fine as URL path segments) data into the path but the / chars get encoded into %2F causing the receiving application to fail parsing the data.
This PR: https://github.com/resteasy/resteasy/pull/945/files seems to allow #PathParam annotated client arguments to be annotated as #Encoded in order to deactivate their url encoding. But trying to use this in quarkus with resteasy-reactive seems to break. The below client definition will always encode / to %2F, whether #Encoded is there or not.
#RegisterRestClient(configKey = "urlpreview")
interface UrlPreviewClient {
#GET
#Path("/{b64url}.jpg")
fun getImage(#Encoded b64url: String): Response
}
I also tried the following client declarations:
#GET
#Path("/{b64url}.jpg")
fun getImage(b64url: String): Response
#GET
#Path("/{b64url}.jpg")
fun getImage(#PathParam("b64url") #Encoded b64url: String): Response
#GET
#Path("/{b64url:.*}.jpg")
fun getImage(#PathParam("b64url") #Encoded b64url: String): Response
All encode / chars in b64url as %2F.

Related

How to set content-type for a spring boot test case which returns PDF file

I am currently testing one of my services with Spring boot test.The service exports all user data and produces a CSV or PDF after successful completion. A file is downloade in browser.
Below is the code i have wrote in my test class
MvcResult result = MockMvc.perform(post("/api/user-accounts/export").param("query","id=='123'")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_PDF_VALUE)
.content(TestUtil.convertObjectToJsonBytes(userObjectDTO)))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_PDF_VALUE))
.andReturn();
String content = result.getResponse().getContentAsString(); // verify the response string.
Below is my resource class code (call comes to this place)-
#PostMapping("/user-accounts/export")
#Timed
public ResponseEntity<byte[]> exportAllUsers(#RequestParam Optional<String> query, #ApiParam Pageable pageable,
#RequestBody UserObjectDTO userObjectDTO) {
HttpHeaders headers = new HttpHeaders();
.
.
.
return new ResponseEntity<>(outputContents, headers, HttpStatus.OK);
}
While I debug my service, and place debug just before the exit, I get content Type as 'application/pdf' and status as 200.I have tried to replicate the same content type in my test case. Somehow it always throws below error during execution -
java.lang.AssertionError: Status
Expected :200
Actual :406
I would like to know, how should i inspect my response (ResponseEntity). Also what should be the desired content-type for response.
You have problem some where else. It appears that an exception/error occurred as noted by application/problem+json content type. This is probably set in the exception handler. As your client is only expecting application/pdf 406 is returned.
You can add a test case to read the error details to know what exactly the error is.
Something like
MvcResult result = MockMvc.perform(post("/api/user-accounts/export").param("query","id=='123'")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_PROBLEM_JSON_VALUE)
.content(TestUtil.convertObjectToJsonBytes(userObjectDTO)))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE))
.andReturn();
String content = result.getResponse().getContentAsString(); // This should show you what the error is and you can adjust your code accordingly.
Going forward if you are expecting the error you can change the accept type to include both pdf and problem json type.
Note - This behaviors is dependent on the spring web mvc version you have.
The latest spring mvc version takes into account the content type header set in the response entity and ignores what is provided in the accept header and parses the response to format possible. So the same test you have will not return 406 code instead would return the content with application json problem content type.
I found the answer with help of #veeram and came to understand that my configuration for MappingJackson2HttpMessageConverter were lacking as per my requirement. I override its default supported Mediatype and it resolved the issue.
Default Supported -
implication/json
application*/json
Code change done to fix this case -
#Autowired
private MappingJackson2HttpMessageConverter jacksonMessageConverter;
List<MediaType> mediaTypes = new ArrayList<>();
mediaTypes.add(MediaType.ALL);
jacksonMessageConverter.setSupportedMediaTypes(mediaTypes);
406 means your client is asking for a contentType (probably pdf) that the server doesn't think it can provide.
I'm guessing the reason your code is working when you debug is that your rest client is not adding the ACCEPT header that asks for a pdf like the test code is.
To fix the issue, add to your #PostMapping annotation produces = MediaType.APPLICATION_PDF_VALUE see https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/PostMapping.html#produces--

Controller returning inputstream - content negotiation and media types

Introduction
I have a question about a RestController and a Test .
I have the following PostMapping:
#PostMapping(path = "/download/as/zip/{zipFileName}" )
#ResponseBody
public ResponseEntity<InputStreamResource> downloadDocumentZip(#RequestHeader(required=false,name="X-Application") String appName, #RequestBody ZipFileModel zipFileModel, #PathVariable("zipFileName") String zipFileName)
And I have the following Test:
Response response = given(this.requestSpecification).port(port)
.filter(document("downloadAsZip",
preprocessRequest(prettyPrint()),
requestHeaders(headerWithName("X-Application").description("Owner application")),
pathParameters(parameterWithName("zipFileName").description("The name of the resulting zip file. Mostly not needed/optional.")))
)
.contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
.header(new Header(HEADER, "themis"))
.body(jsonContent)
.when()
.post("/download/as/zip/{zipFileName}", "resultFile.zip");
This works and 200 is returned.
First Question
Now I am a bit confused about the meaning of .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE) within the Test.
Content-type is the header on the returned response. But in this test it is being included while making the test request ? Or is it signifying in this case that we are sending JSON in the request body?
Second Question
I know that my controller method should consume JSON, and returns Bytes.
So hence, I make the following change:
#PostMapping(path = "/download/as/zip/{zipFileName}", consumes = MediaType.APPLICATION_JSON_VALUE)
This works so far.
So then I add the following:
#PostMapping(path = "/download/as/zip/{zipFileName}", consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
And it fails:
java.lang.AssertionError:
Expected :200
Actual :406
<Click to see difference>
So I changed my test to be the following:
.contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
.accept(MediaType.APPLICATION_OCTET_STREAM_VALUE)
This fails once again.
Expected :200
Actual :406
So even though the client is signalling the same accept header as what the controller produces, we have an error.
Questions:
So should we or should we not have the produces= on the request mapping?
Why is it failing now? Is there a conflict in consuming JSON and Producing Bytes? Or the ContentType within the test?
The problem is that spring changes the return content-type if the end of a URL has an extension.
So seeing .zip at the end, was causing spring to over-ride the type to application/zip .

How to make Feign POST request without a request body and with query params?

I am using Feign with the Apache Http Client and I would like to support the following jax-rs interface:
#POST
#Path("/do_something")
void doSomething(#QueryParam("arg") String arg);
But, ApacheHttpClient uses a RequestBuilder, which converts query parameters for requests without a body/entity into a UrlEncodedFormEntity.
I am converting my APIs to jax-rs, and I do not want to break backwards compatibility. Is there a way to use Feign without adjusting my API? Will the OkHttp or Ribbon clients support POSTs with query params and no body/entity? Is there another java jax-rs client that will support this?
Also, is there a reason why RequestBuilder turns query params into a UrlEncodedFormEntity? Is there an alternative HttpUriRequest builder within the apache-httpclient library that doesn't do this? RequestBuilder's build method has the following lines of code:
if (entity == null && (HttpPost.METHOD_NAME.equalsIgnoreCase(method) || HttpPut.METHOD_NAME.equalsIgnoreCase(method))) {
entity = new UrlEncodedFormEntity(parameters, HTTP.DEF_CONTENT_CHARSET);
} else {
// omitted expected behavior
}
Before switching to Feign, my code constructed a HttpUriRequest with something similar to the following:
URI uri = new URIBuilder()
.setScheme("https")
.setHost("localhost")
.setPath("service/do_something")
.addParameter("arg", "value")
.build();
HttpUriRequest request = new HttpPost(uri);
If you are willing to break the API slightly and maintain support for the #QueryParam, then you could define a request interceptor on the feign client that adds a plain text entity/body to the request:
.requestInterceptor(template -> {
if (template.method().equals(HttpPost.METHOD_NAME) && template.queries().keySet().size() > 0 && template.body() == null) {
template.body(" ");
}
})
Then, your API would change with the following:
#POST
#Consumes(MediaType.TEXT_PLAIN)
#Path("/do_something")
void doSomething(#QueryParam("arg") String arg);
But, this breaks the API since the server now expects/consumes a POST message with a plain text entity/body.
I think the same could be accomplished without the requestInterceptor and with Feign's #Body template:
#POST
#Consumes(MediaType.TEXT_PLAIN)
#Body(" ")
#Path("/do_something")
void doSomething(#QueryParam("arg") String arg);
But, this means that your API would have to include Feign rather than pure jax-rs annotations.

How to measure REST payload size?

i am using Spring-REST and JSON payload, is there an easy way to measure the payload size ?
My Rest controller returns a list of object, so i don't have a direct access to the JSON String sent to the client.
#RestController
#RequestMapping("/films")
public class FilmController {
#RequestMapping(method = RequestMethod.GET)
public List<Film> filmAll() {
List<Film> films = filmDao.findAllManyToOneQueryDsl();
return films;
}
}
There is the Content-Length HTTP header, though it may or may not be there. It is supplied by the other side (client or server). You will also have to trust the other side to provide the correct value for Content-Length. Other than that, the payload is usually presented to you as a byte stream (or character stream). You can consume the stream and count the bytes (or characters).

FileSystemResource is returned with content type json

I have the following spring mvc method that returns a file:
#RequestMapping(value = "/files/{fileName}", method = RequestMethod.GET)
public FileSystemResource getFiles(#PathVariable String fileName){
String path="/home/marios/Desktop/";
return new FileSystemResource(path+fileName);
}
I expect a ResourceHttpMessageConverter to create the appropriate response with an octet-stream type according to its documentation:
If JAF is not available, application/octet-stream is used.
However although I correctly get the file without a problem, the result has Content-Type: application/json;charset=UTF-8
Can you tell me why this happens?
(I use spring version 4.1.4. I have not set explicitly any message converters and I know that spring loads by default among others the ResourceHttpMessageConverter and also the MappingJackson2HttpMessageConverter because I have jackson 2 in my classpath due to the fact that I have other mvc methods that return json.
Also if I use HttpEntity<FileSystemResource> and set manually the content type, or specify it with produces = MediaType.APPLICATION_OCTET_STREAM it works fine.
Note also that in my request I do not specify any accept content types, and prefer not to rely on my clients to do that)
I ended up debugging the whole thing, and I found that AbstractJackson2HttpMessageConverter has a canWrite implementation that returns true in case of the FileSystemResource because it just checks if class is serializable, and the set media type which is null since I do not specify any which in that case is supposed to be supported by it.
As a result it ends up putting the json content types in a list of producible media types. Of course ResourceHttpMessageConverter.canWrite implementation also naturally returns true, but the ResourceHttpMessageConverter does not return any producible media types.
When the time to write the actual response comes, from the write method implementation, the write of the ResourceHttpMessageConverter runs first due to the fact that the ResourceHttpMessageConverter is first in the list of the available converters (if MappingJackson2HttpMessageConverter was first, it would try to call write since its canWrite returns true and throw exception), and since there was already a producible content type set, it does not default to running the ResourceHttpMessageConverter.getDefaultContentType that would set the correct content type.
If I remove json converter all would work fine, but unfortunately none of my json methods would work. Therefore specifying the content type is the only way to get rid of the returned json content type
For anyone still looking for a piece of code:
You should wrap your FileSystemResource into a ResponseEntity<>
Then determine your image's content type and append it to ResponseEntity as a header.
Here is an example:
#GetMapping("/image")
public #ResponseBody ResponseEntity<FileSystemResource> getImage() throws IOException {
File file = /* load your image file from anywhere */;
if (!file.exists()) {
//TODO: throw 404
}
FileSystemResource resource = new FileSystemResource(file);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(/* determine your image's media type or just set it as a constant using MediaType.<value> */);
headers.setContentLength(resource.contentLength());
return new ResponseEntity<>(resource, headers, HttpStatus.OK);
}

Resources