Why is Retrofit2 adding quotes to my POST body? - java

I have one API request where the POST body is hex-encoded binary data transmitted as plain text. For reasons which I cannot discern, Retrofit2 is adding quotes around the hex-encoded string before adding it to the request, which causes the server to choke on it and complain about a malformed request.
We're in the middle of converting our app from original Retrofit to Retrofit2, and none of the payload generation code has changed at all.
I've been able to work around this problem by using an Interceptor to remove the enclosing quotes from the request body at runtime, but that seems like a really daft hoop to have to jump through and I'd much rather not have the quotes appear in the first place.
My interface looks like this:
public interface SampleApi {
#POST("sample-endpoint")
Call<ApiResponse> postThing(#Body String hexEncodedData,
#Header(HttpHeaders.DATE) String gmtDateStr,
#Header("X-CUSTOM-ONE") long custom1,
#Header("X-CUSTOM-TWO") String custom2);
}
I've experimented with setting the Content-Type header to various values with no apparent effect. I haven't built a custom type converter yet because having to make one of those for a plain old string seems like it shouldn't be necessary.
Can someone tell me what I'm doing wrong?

Double quotes is logical, as retrofit is sending data in json format so double quotes if type String . Try this it might help you .
public interface SampleApi {
#POST("sample-endpoint")
Call<ApiResponse> postThing(#Body RequestBody hexEncodedData,
#Header(HttpHeaders.DATE) String gmtDateStr,
#Header("X-CUSTOM-ONE") long custom1,
#Header("X-CUSTOM-TWO") String custom2);
}
String strRequestBody = "body";
RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain"),strRequestBody);

Update, for Kotlin the Code_Life code should look like this:
interface SampleApi {
#POST("sample-endpoint")
suspend fun postThing(
#Body presentation: RequestBody,
#Header(HttpHeaders.DATE) gmtDateStr: String,
#Header("X-CUSTOM-ONE") custom1: Long,
#Header("X-CUSTOM-TWO") custom2: String
): Call<ApiResponse>
}
val strRequestBody = "body"
val requestBody = strRequestBody.toRequestBody("text/plain".toMediaTypeOrNull()

Related

Retrofit2 complex request

I am trying to create a request using Retrofit2. I created the request using a standard library:
path = "https://www.iii.com/?id="+id+"&data=";
query = "{\"name\":\""+name+"\",\"quantity\":20}";
Final link is:
link = path+URLEncoder.encode(query, "UTF-8");
I tried different Retrofit2 options, but I can't understand how to translate my link to Retrofit2 link using together path and query with url encoded?
You can parse GET query parameters to retrofit using this code:
#GET("https://www.iii.com")
Observable<ResponseBody> getSomething(
#Query("id") int id,
#Query("data") String data
);
Retrofit will build it for you. Just pass your variables (assuming you know how to call retrofit requests) and retrofit will url encode it for you. You can refer to this link: https://square.github.io/retrofit/2.x/retrofit/index.html?retrofit2/http/Query.html
Values are converted to strings using Retrofit.stringConverter(Type, Annotation[]) (or Object.toString(), if no matching string converter is installed) and then URL encoded. null values are ignored. Passing a List or array will result in a query parameter for each non-null item.
you can add anotation for that like below
#Headers("charset=UTF-8")
#GET("https://www.iii.com")
Observable<ResponseBody> getSomething(
#Query("id") int id,
#Query("data") String data
);

Spring RestTemplate getForObject URL not working for Apple iTunes

I created the following simple test to query iTunes:
#Test
fun loadArtist()
{
val restTemplate = RestTemplate()
val builder = UriComponentsBuilder.fromHttpUrl("https://itunes.apple.com/search")
builder.queryParam("term", "howling wolf")
builder.queryParam("entity", "allArtist")
builder.queryParam("limit", 1)
println("\n\nURL ${builder.toUriString()}")
val result = restTemplate.getForObject(builder.toUriString(), String::class.java);
println("Got artist: $result")
}
And the output was unexpected:
URL https://itunes.apple.com/search?term=howling%20wolf&entity=allArtist&limit=1
Got artist:
{
"resultCount":0,
"results": []
}
Pasting the generated URL into a browser does give expected results - artist returned.
https://itunes.apple.com/search?term=howling%20wolf&entity=allArtist&limit=1
Also, hard-coding the query works:
val result = restTemplate.getForObject("https://itunes.apple.com/search?term=howling%20wolf&entity=allArtist&limit=1", String::class.java);
. . the problem only seems to occur for term queries that include spaces.
What went wrong? Other than assemble the URL by hand, how to fix?
Seems like a case of double encoding the whitespace. From the RestTemplate Javadoc:
For each HTTP method there are three variants: two accept a URI
template string and URI variables (array or map) while a third accepts
a URI. Note that for URI templates it is assumed encoding is
necessary, e.g. restTemplate.getForObject("http://example.com/hotel
list") becomes "http://example.com/hotel%20list". This also means if
the URI template or URI variables are already encoded, double encoding
will occur, e.g. http://example.com/hotel%20list becomes
http://example.com/hotel%2520list). To avoid that use a URI method
variant to provide (or re-use) a previously encoded URI. To prepare
such an URI with full control over encoding, consider using
UriComponentsBuilder.
So it looks like getForObject will actually query for https://itunes.apple.com/search?term=howling%2520wolf&entity=allArtist&limit=1 and thus result in an empty result. You can always just replace whitespaces with a "+" in your term or try to make one of those classes skip the encoding process.

retrofit not valid URI

Hi I'm trying to do a simple http get query via Retrofit.
My parameter has some special characters and it seems that the url encoding fails.
Original:
data=[out:json];node["name"~"Karlsruhe"]["place"~"city|village|town"];out body;
correct encoding should look like this:
data=%5Bout%3Ajson%5D%3Bnode%5B%22name%22~%22Karlsruhe%22%5D%5B%22place%22~%22city%7Cvillage%7Ctown%22%5D%3Bout%20body%3B
but Retrofit creates this:
data=[out:json];node[%22name%22~%22Karlsruhe%22][%22place%22~%22city|village|town%22];out%20body;
and this will fail with:
java.lang.IllegalStateException: not valid as a java.net.URI:
http://overpass.osm.rambler.ru/cgi/interpreter?data=[out:json];node[%22name%22~%22Karlsruhe%22][%22place%22~%22city|village|town%22];out%20body;
at com.squareup.okhttp.HttpUrl.uri(HttpUrl.java:336) at
com.squareup.okhttp.internal.http.RouteSelector.resetNextProxy(RouteSelector.java:135)
at
com.squareup.okhttp.internal.http.RouteSelector.(RouteSelector.java:71)
at
com.squareup.okhttp.internal.http.RouteSelector.get(RouteSelector.java:76)
at
com.squareup.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:321)
at
com.squareup.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:245)
at com.squareup.okhttp.Call.getResponse(Call.java:267) at
com.squareup.okhttp.Call$ApplicationInterceptorChain.proceed(Call.java:224)
at
com.squareup.okhttp.Call.getResponseWithInterceptorChain(Call.java:195)
at com.squareup.okhttp.Call.execute(Call.java:79) at
retrofit.OkHttpCall.execute(OkHttpCall.java:112)
What can be done here to fix this encoding issue?
Thanks
I am not sure about what the root cause of the encoding error is, but you can work around it with the encoded parameter to the Query notation. Setting the parameter to true tells retrofit the parameter is already encoded, so do not encode again.
In your service interface, add encoded=true to your #Query annotation. Something like --
Call<ResponseBody> getResponse(#Query(value = "data", encoded = true) String data);
Then, encode the parameter yourself before sending to retrofit.
final String encodedData = URLEncoder.encode(data, "UTF-8");
Call<ResponseBody> result = service.getResponse(encodedData);

Jersey client send string with application/json type

I need to send a String that is already in JSON format using the Jersey client 1.19 and genson 1.3
Client.create().resource(path).webResource.type(MediaType.APPLICATION_JSON_TYPE)
.accept(MediaType.APPLICATION_JSON_TYPE).put(ClientResponse.class, jsonAsString)
The problem with this is that the client is parsing the string, I need it to be sent as it is.
I'm sending something like { "name":"Foo" } and the client is converting it to "{ \"name\":\"Foo\" }". If I change the type to PLAIN_TEXT it sends the request correctly but I need to send it as application/json .
So yes Genson will try to encode your string as a literal json string. In this case it is maybe not what you would want, but more generally it is what people would expect: serialize à java string as a json string.
The solution I see is too extend GensonJsonConverter and override isWritable to return false when the input type is string. Then just register it. That should work.
I've opened this issue so it can be added as a more flexible feature.
Try changing MediaType.APPLICATION_JSON_TYPE to MediaType.APPLICATION_JSON
http://examples.javacodegeeks.com/enterprise-java/rest/jersey/json-example-with-jersey-jackson/
I tried your code with Jersey 1.19, Genson 1.3 and Wireshark. It works fine without Genson so it appears Genson is treating it as a literal string (since it is of type String) and therefore quoting the double quotes.
The following works.
String jsonAsString = "{ \"name\":\"Foo\" }";
Map<String, String> map = (new Genson()).deserialize(jsonAsString, Map.class);
String path = "...";
ClientResponse resp =
Client.create().resource(path)
.type(MediaType.APPLICATION_JSON_TYPE)
.accept(MediaType.APPLICATION_JSON_TYPE)
.put(ClientResponse.class, map);
I have used Map because it is sufficient for the example but you can deserialize it to the appropriate object.
If you looking for an option to specify that the string should be passed as is, I am not yet aware of it but this should at least provide a solution to the problem of sending that string as application/json.

Jersey Post request - How to perform a file upload with an unknown number of additional parameters?

I asked something like this previously, but upon re-reading my original post, it was not easy to understand what I was really asking. I have the following situation. We have (or at least I'm trying to get working) a custom file upload procedure that will take in the file, a set number of 'known' metadata values (and they will always be there), as well as potentially an unknown number of additional metadata values. The service that exists currently uses the Jersey framework (1.16)
I currently have both client and server code that handles dealing with the file upload portion and the known metadata values (server code below)
#POST
#Path("asset/{obfuscatedValue0}/")
#Consumes(MediaType.MULTIPART_FORM_DATA)
public UUID uploadBlob(#PathParam("obfuscatedValue0") Integer obfuscatedValue0,
#FormDataParam("obfuscatedValue1") String obfuscatedValue1,
#FormDataParam("obfuscatedValue2") String obfuscatedValue2,
#FormDataParam("obfuscatedValue3") String obfuscatedValue3,
#FormDataParam("obfuscatedValue4") String obfuscatedValue4,
#FormDataParam("obfuscatedValue5") String obfuscatedValue5,
#FormDataParam("file") InputStream uploadedInputStream) {
.....
}
...and excerpt of client code:
Builder requestBuilder = _storageService
.path("asset")
.path(obfuscatedValue0.toString())
.type(MediaType.MULTIPART_FORM_DATA)
.accept(MediaType.APPLICATION_JSON);
FormDataMultiPart part = new FormDataMultiPart()
.field("file", is, MediaType.TEXT_PLAIN_TYPE) // 'is' is an inputstream from earlier in code.
.field("obfuscatedValue1", obfuscatedValue1)
.field("obfuscatedValue2", obfuscatedValue2)
.field("obfuscatedValue3", obfuscatedValue3)
.field("obfuscatedValue4", obfuscatedValue4)
.field("obfuscatedValue5", obfuscatedValue5);
storedAsset = requestBuilder.post(UUID.class, part);
However, I need to pass a map of additional parameters that will have an unknown number of values/names. From what I've seen, there is no easy way to do this using the FormDataParam annotation like my previous example.
Based upon various internet searches related to Jersey file uploads, I've attempted to convert it to use MultivaluedMap with the content type set to "application/x-www-form-urlencoded" so it resembles this:
#POST
#Path("asset/{value}/")
#Consumes("application/x-www-form-urlencoded")
public UUID uploadBlob(#PathParam(value), MultivaluedMap<String,String> formParams) {
....
}
It's my understanding that MultivaluedMap is intended to obtain a general map of form parameters (and as such, cannot play nicely together in the same method bearing #FormDataParam annotations.) If I can pass all this information from the Client inside some sort of map, I think I can figure out how to handle parsing the map to grab and 'doMagic()' on the data to get what I want done; I don't think I'll have a problem there.
What I AM fairly confused about is how to format the request client-side code when using this second method within the jersey framework. Can anyone provide some guidance for the situation, or some suggestions on how to proceed? I'm considering trying the solution proposed here and developing a custom xml adapter to deal with this situation, and sending xml instead of multipart-form-data but I'm still confused how this would interact with the InputStream value that will need to be passed. It appears the examples with MultivaluedMap that I've seen only deal with String data.

Categories