java.net.SocketException: Unexpected end of file from server using Spring's RestTemplate - spring

I've already checked several questions / answers regarding similar subjects, but can't find the proper answer for my case.
I'm using Spring's RestTemplate but fails to get the response from a third party server with the following exception:
Exception in thread "main" org.springframework.web.client.ResourceAccessException: I/O error on POST request for "http://64.76.157.19:8283/ENAP/ProveedorExterno/v1.0/insertarUltimaPosicion":Unexpected end of file from server; nested exception is java.net.SocketException: Unexpected end of file from server
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:567)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:512)
at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:454)
at cl.waypoint.integracion.GenericCallback.sendEnap(GenericCallback.java:187)
at cl.waypoint.integracion.GenericCallback.main(GenericCallback.java:167)
Caused by: java.net.SocketException: Unexpected end of file from server
at sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:718)
at sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:579)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1322)
at java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:468)
at org.springframework.http.client.SimpleClientHttpResponse.getRawStatusCode(SimpleClientHttpResponse.java:48)
at cl.waypoint.integracion.GenericCallback$LoggingRequestInterceptor.log(GenericCallback.java:229)
at cl.waypoint.integracion.GenericCallback$LoggingRequestInterceptor.intercept(GenericCallback.java:216)
at org.springframework.http.client.InterceptingClientHttpRequest$RequestExecution.execute(InterceptingClientHttpRequest.java:84)
at org.springframework.http.client.InterceptingClientHttpRequest.executeInternal(InterceptingClientHttpRequest.java:69)
at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48)
at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:53)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:551)
... 4 more
But when sending the same request/headers/payload using command line curl there seems to be no problem at all, this is the verbose output of it:
* Trying A.B.C.D...
* Connected to A.B.C.D (A.B.C.D) port 8283 (#0)
> POST /ENAP/ProveedorExterno/v1.0/insertarUltimaPosicion HTTP/1.1
> Host: A.B.C.D:8283
> User-Agent: curl/7.43.0
> Accept: */*
> Content-Type: application/json
> Authorization: Bearer dsgfsdgf786dsfg7dsgf
> Content-Length: 567
>
* upload completely sent off: 567 out of 567 bytes
< HTTP/1.1 200 OK
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: POST
< Access-Control-Allow-Headers: authorization,Access-Control-Allow-Origin,Content-Type
< Content-Type: application/json
< Date: Wed, 27 Jul 2016 13:35:26 GMT
< Transfer-Encoding: chunked
<
* Connection #0 to host 64.76.157.19 left intact
PS: Authorization token and server's IP address have been changed for security reasons.
Spring seems to hang for a while and then then throw the exception, perhaps it's waiting for something by default...Content-Length header on the response? If so, can that be overriden?
The Exception comes from the following interceptor:
class LoggingRequestInterceptor implements ClientHttpRequestInterceptor {
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
ClientHttpResponse response = execution.execute(request, body);
log(request, body, response);
return response;
}
private void log(HttpRequest request, byte[] body, ClientHttpResponse response) throws IOException {
HttpHeaders headers = request.getHeaders();
System.out.println("=============================");
for (Entry<String, List<String>> header : headers.entrySet()) {
System.out.println(header.getKey() + ": " + header.getValue());
}
System.out.println("=============================");
System.out.println(new String(body));
System.out.println(response.getRawStatusCode());
System.out.println(response.getStatusText());
}
}
Which is used from the following code snippet:
private void sendEnap(String patente, String fecha, String latitud, String longitud, BigInteger sentido,
BigInteger velocidad, int ignicion) {
RestTemplate restTemplate = new RestTemplate();
// set interceptors/requestFactory
ClientHttpRequestInterceptor ri = new LoggingRequestInterceptor();
List<ClientHttpRequestInterceptor> ris = new ArrayList<ClientHttpRequestInterceptor>();
ris.add(ri);
restTemplate.setInterceptors(ris);
MultiValueMap<String, String> headers = new LinkedMultiValueMap<String, String>();
headers.add("Authorization", "Bearer " + ENANGAB_TOKEN);
headers.add("Content-Type", MediaType.APPLICATION_JSON.toString());
headers.add("User-Agent", "Waypoint");
EnapRequest enapRequest = new EnapRequest(patente, fecha, latitud, longitud, sentido, velocidad, ignicion);
HttpEntity<EnapRequest> request = new HttpEntity<EnapRequest>(enapRequest, headers);
ResponseEntity<EnapResponse> response = restTemplate.exchange(ENAP_ENDPOINT, HttpMethod.POST, request,
EnapResponse.class);
System.out.println(response.getBody());
}
If the interceptor is disabled, same exception arises but now with this stacktrace:
Exception in thread "main" org.springframework.web.client.ResourceAccessException: I/O error on POST request for "http://64.76.157.19:8283/ENAP/ProveedorExterno/v1.0/insertarUltimaPosicion":Unexpected end of file from server; nested exception is java.net.SocketException: Unexpected end of file from server
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:567)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:512)
at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:454)
at cl.waypoint.integracion.GenericCallback.sendEnap(GenericCallback.java:187)
at cl.waypoint.integracion.GenericCallback.main(GenericCallback.java:167)
Caused by: java.net.SocketException: Unexpected end of file from server
at sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:718)
at sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:579)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1322)
at java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:468)
at org.springframework.http.client.SimpleClientHttpResponse.getRawStatusCode(SimpleClientHttpResponse.java:48)
at org.springframework.http.client.AbstractClientHttpResponse.getStatusCode(AbstractClientHttpResponse.java:33)
at org.springframework.web.client.DefaultResponseErrorHandler.getHttpStatusCode(DefaultResponseErrorHandler.java:56)
at org.springframework.web.client.DefaultResponseErrorHandler.hasError(DefaultResponseErrorHandler.java:50)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:552)
... 4 more
The above is another hint for a missing header on the response, don't know which one, and how to avoid such wait too.
Any hints would be appreciated
EDIT:
Headers sent:
Accept: [application/json, application/*+json]
Authorization: [Bearer dsgfsdgf786dsfg7dsgf]
Content-Type: [application/json]
User-Agent: [Waypoint]
Content-Length: [567]
EnapRequest class:
package cl.waypoint.integracion;
import java.math.BigInteger;
import com.fasterxml.jackson.annotation.JsonProperty;
public class EnapRequest {
#JsonProperty("token_proveedor")
private String tokenProveedor = GenericCallback.ENANGAB_TOKEN;
private Posicion[] posicion;
public EnapRequest(String patente, String fecha, String latitud, String longitud, BigInteger sentido,
BigInteger velocidad, int ignicion) {
posicion = new Posicion[1];
posicion[0] = new Posicion(patente, fecha, latitud, longitud, sentido, velocidad, ignicion);
}
public String getTokenProveedor() {
return tokenProveedor;
}
public void setTokenProveedor(String tokenProveedor) {
this.tokenProveedor = tokenProveedor;
}
public Posicion[] getPosicion() {
return posicion;
}
public void setPosicion(Posicion[] posicion) {
this.posicion = posicion;
}
}
The request body is in fact being sent as JSON (exactly the same as with curl, pretty print here for improved reading):
{
"posicion": [
{
"patente": "AB1234",
"latitud": "-36.752752",
"longitud": "-73.0804947",
"direccion": "120",
"velocidad": "65",
"transportista": "ENANGAB",
"sensora1": null,
"sensora2": null,
"sensora3": null,
"mopo_sensord1": null,
"mopo_sensord2": null,
"mopo_sensord3": null,
"mopo_sensord4": null,
"mopo_sensord5": null,
"mopo_sensord6": null,
"opcional1": null,
"opcional2": null,
"opcional3": null,
"opcional4": null,
"codigo_interno": null,
"fecha_hora": "2016-07-15T14:24:00",
"mopo_estado": "1",
"mopo_estado_ignicion": "1",
"moev_numero_evento": "45"
}
],
"token_proveedor": "dsgfsdgf786dsfg7dsgf"
}
The RestTemplate object has already configured support for the following converters:
class org.springframework.http.converter.ByteArrayHttpMessageConverter
class org.springframework.http.converter.StringHttpMessageConverter
class org.springframework.http.converter.ResourceHttpMessageConverter
class org.springframework.http.converter.xml.SourceHttpMessageConverter
class org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter
class org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter
class org.springframework.http.converter.json.MappingJackson2HttpMessageConverter

I think the problem here is that your request has a wrong data type which server can not parse and thus can not reply.
Since you are sending a POST request with JSON Content-Type header, your EnapRequest must be JSON-encoded.
To do that, you need to make sure EnapRequest is a POJO class, then modify your code inside sendEnap()
RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
and include Jackson libraries in the classpath
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.1</version>
</dependency>

I had the same error, check the packet size configured at the network level for the server(if there is a mismatch - the packet size expected is lesser than the packet size recieved, this error would arise)
Try running the following command on the server you are hitting:
ip addr | grep mtu

Related

Spring WebServiceTemplate add value to Cookie header

I have to add an information in the cookie of a request that my application sends to another application,
but it doesn't seem to be added correctly.
When I check the request with WireShark, I see two Cookie headers in the headers :
POST /service HTTP/1.1
Accept-Encoding: gzip
Cookie: iam=**************************
Accept: text/xml, text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
SOAPAction: ""
Content-Type: text/xml; charset=utf-8
Content-Length: 128393
Host: host-dev:9999
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.5.10 (Java/1.8.0_271)
Cookie: JSESSIONID=***********************
Authorization: Basic **************************
(I've changed some of the info)
In my code I have this :
#Service
public class ESignatureSoapConnector extends WebServiceGatewaySupport {
private ObjectFactory objectFactory;
#Autowired
public ESignatureSoapConnector(ESignatureMarshaller marshaller, ConfigurationProperties configurationProperties) throws Exception {
this.setMarshaller(marshaller);
this.setUnmarshaller(marshaller);
this.setDefaultUri(configurationProperties.getBaseUrl());
this.setMessageSender(buildMessageSender(configurationProperties.getUsername(), configurationProperties.getPassword()));
this.objectFactory = new ObjectFactory();
}
public ESignatureResponse signDocument(MTOMFile file, String iamCookieValue) {
ESignature request = new ESignature();
request.setInputDocument(file);
JAXBElement<ESignatureResponse> response = (JAXBElement<ESignatureResponse>) getWebServiceTemplate()
.marshalSendAndReceive(objectFactory.createESignature(request), new WebServiceMessageCallback() {
#Override
public void doWithMessage(WebServiceMessage webServiceMessage) throws IOException, TransformerException {
TransportContext context = TransportContextHolder.getTransportContext();
HttpComponentsConnection connection = (HttpComponentsConnection) context.getConnection();
HttpPost post = connection.getHttpPost();
post.addHeader("Cookie", "iam=" + iamCookieValue);
}
});
return response.getValue();
}
private WebServiceMessageSender buildMessageSender(String username, String password) throws Exception {
...
}
}
I'm assuming the way I set the cookie isn't correct but I can't find the proper way to do it.
The value for the cookie is different for each request, it's a soap request and I work in Spring
The solution we've found :
JAXBElement<ESignatureResponse> response = (JAXBElement<ESignatureResponse>) getWebServiceTemplate()
.marshalSendAndReceive(objectFactory.createESignature(request), new WebServiceMessageCallback() {
#Override
public void doWithMessage(WebServiceMessage webServiceMessage) throws IOException, TransformerException {
HttpClient httpClient = ((HttpComponentsMessageSender) getWebServiceTemplate().getMessageSenders()[0]).getHttpClient();
BasicClientCookie iamCookie = new BasicClientCookie(iamConfigurationProperties.getCookieName(), iamCookieValue);
iamCookie.setDomain(iamConfigurationProperties.getCookieDomain());
iamCookie.setPath(iamConfigurationProperties.getCookiePath());
((DefaultHttpClient) httpClient).getCookieStore().addCookie(iamCookie);
}
});

Request with multipart/form-data returns 415 error

I need to receive this request using Spring:
POST /test HTTP/1.1
user-agent: Dart/2.8 (dart:io)
content-type: multipart/form-data; boundary=--dio-boundary-3791459749
accept-encoding: gzip
content-length: 151
host: 192.168.0.107:8443
----dio-boundary-3791459749
content-disposition: form-data; name="MyModel"
{"testString":"hello world"}
----dio-boundary-3791459749--
But unfortunately this Spring endpoint:
#PostMapping(value = "/test", consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public void test(#Valid #RequestPart(value = "MyModel") MyModel myModel) {
String testString = myModel.getTestString();
}
returns 415 error:
Content type 'multipart/form-data;boundary=--dio-boundary-2534440849' not supported
to the client.
And this(same endpoint but with the consumes = MULTIPART_FORM_DATA_VALUE):
#PostMapping(value = "/test", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void test(#Valid #RequestPart(value = "MyModel") MyModel myModel) {
String testString = myModel.getTestString();
}
again returns 415 but, with this message:
Content type 'application/octet-stream' not supported
I already successfully used this endpoint(even without consumes) with this old request:
POST /test HTTP/1.1
Content-Type: multipart/form-data; boundary=62b81b81-05b1-4287-971b-c32ffa990559
Content-Length: 275
Host: 192.168.0.107:8443
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.8.0
--62b81b81-05b1-4287-971b-c32ffa990559
Content-Disposition: form-data; name="MyModel"
Content-Transfer-Encoding: binary
Content-Type: application/json; charset=UTF-8
Content-Length: 35
{"testString":"hello world"}
--62b81b81-05b1-4287-971b-c32ffa990559--
But unfortunately now I need to use the first described request and I can't add additional fields to it.
So, I need to change the Spring endpoint, but how?
You need to have your controller method consume MediaType.MULTIPART_FORM_DATA_VALUE,
#PostMapping(value = "/test", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
......
You also need to add a MappingJackson2HttpMessageConverter support application/octet-stream. In this answer,
I configure it by using WebMvcConfigurer#extendMessageConverters so that I can keep the default configuration of the other converters.(Spring MVC is configured with Spring Boot’s converters).
I create the converter from the ObjectMapper instance used by Spring.
[For more information]
Spring Boot Reference Documentation - Spring MVC Auto-configuration
How do I obtain the Jackson ObjectMapper in use by Spring 4.1?
Why does Spring Boot change the format of a JSON response even when a custom converter which never handles JSON is configured?
#Configuration
public class MyConfigurer implements WebMvcConfigurer {
#Autowired
private ObjectMapper objectMapper;
#Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
ReadOnlyMultipartFormDataEndpointConverter converter = new ReadOnlyMultipartFormDataEndpointConverter(
objectMapper);
List<MediaType> supportedMediaTypes = new ArrayList<>();
supportedMediaTypes.addAll(converter.getSupportedMediaTypes());
supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
converter.setSupportedMediaTypes(supportedMediaTypes);
converters.add(converter);
}
}
[NOTE]
Also you can modify the behavior of your converter by extending it.
In this answer, I extends MappingJackson2HttpMessageConverter so that
it reads data only when the mapped controller method consumes just MediaType.MULTIPART_FORM_DATA_VALUE
it doesn't write any response(another converter do that).
public class ReadOnlyMultipartFormDataEndpointConverter extends MappingJackson2HttpMessageConverter {
public ReadOnlyMultipartFormDataEndpointConverter(ObjectMapper objectMapper) {
super(objectMapper);
}
#Override
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
// When a rest client(e.g. RestTemplate#getForObject) reads a request, 'RequestAttributes' can be null.
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
return false;
}
HandlerMethod handlerMethod = (HandlerMethod) requestAttributes
.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
if (handlerMethod == null) {
return false;
}
RequestMapping requestMapping = handlerMethod.getMethodAnnotation(RequestMapping.class);
if (requestMapping == null) {
return false;
}
// This converter reads data only when the mapped controller method consumes just 'MediaType.MULTIPART_FORM_DATA_VALUE'.
if (requestMapping.consumes().length != 1
|| !MediaType.MULTIPART_FORM_DATA_VALUE.equals(requestMapping.consumes()[0])) {
return false;
}
return super.canRead(type, contextClass, mediaType);
}
// If you want to decide whether this converter can reads data depending on end point classes (i.e. classes with '#RestController'/'#Controller'),
// you have to compare 'contextClass' to the type(s) of your end point class(es).
// Use this 'canRead' method instead.
// #Override
// public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
// return YourEndpointController.class == contextClass && super.canRead(type, contextClass, mediaType);
// }
#Override
protected boolean canWrite(MediaType mediaType) {
// This converter is only be used for requests.
return false;
}
}
The causes of 415 errors
When your controller method consumes MediaType.APPLICATION_OCTET_STREAM_VALUE, it doesn't handle a request with Content-Type: multipart/form-data;. Therefore you get 415.
On the other hand, when your controller method consumes MediaType.MULTIPART_FORM_DATA_VALUE, it can handle a request with Content-Type: multipart/form-data;. However JSON without Content-Type is not handled depending on your configuration.
When you annotate a method argument with #RequestPart annotation,
RequestPartMethodArgumentResolver parses a request.
RequestPartMethodArgumentResolver recognizes content-type as application/octet-stream when it is not specified.
RequestPartMethodArgumentResolver uses a MappingJackson2HttpMessageConverter to parse a reuqest body and get JSON.
By default configuration MappingJackson2HttpMessageConverter supports application/json and application/*+json only.
(As far as I read your question) Your MappingJackson2HttpMessageConverters don't seem to support application/octet-stream.(Therefore you get 415.)
Conclusion
Therefore I think you can successfully handle a request by letting MappingJackson2HttpMessageConverter(an implementation of HttpMessageConverter) to support application/octet-stream like above.
[UPDATE 1]
If you don't need to validate MyModel with #Valid annotation and simply want to convert the JSON body to MyModel, #RequestParam can be useful.
If you choose this solution, you do NOT have to configure MappingJackson2HttpMessageConverter to support application/octet-stream.
You can handle not only JSON data but also file data using this solution.
#PostMapping(value = "/test", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void test(#RequestParam(value = "MyModel") Part part) throws IOException {
// 'part' is an instance of 'javax.servlet.http.Part'.
// According to javadoc of 'javax.servlet.http.Part',
// 'The part may represent either an uploaded file or form data'
try (InputStream is = part.getInputStream()) {
ObjectMapper objectMapper = new ObjectMapper();
MyModel myModel = objectMapper.readValue(part.getInputStream(), MyModel.class);
.....
}
.....
}
See Also
Javadoc of RequestPartMethodArgumentResolver
Javadoc of MappingJackson2HttpMessageConverter
Content type blank is not supported (Related question)
Spring Web MVC - Multipart

Webflux Spring 2.1.2 customize Content-Type

I am trying to post via WebClient to get microsoft token:
public WebClient getWebclient() {
TcpClient client = TcpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
.doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(15)).addHandlerLast(new WriteTimeoutHandler(15)));
ExchangeStrategies strategies = ExchangeStrategies.builder()
.codecs(configurer -> {
configurer.registerDefaults(true);
FormHttpMessageReader formHttpMessageReader = new FormHttpMessageReader();
formHttpMessageReader.setEnableLoggingRequestDetails(true);
configurer.customCodecs().reader(formHttpMessageReader);
})
.build();
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(HttpClient.from(client).followRedirect(true)))
.exchangeStrategies(strategies)
.filter(logRequest())
.filter(logResponse())
.build();
}
MultiValueMap<String, String> credentials = new LinkedMultiValueMap<>();
credentials.add("grant_type", "password");
credentials.add("client_id", oauthClientId);
credentials.add("resource", oauthResource);
credentials.add("scope", oauthScope);
credentials.add("username", oauthUsername);
credentials.add("password", oauthPassword);
Mono<MicrosoftToken> response = webClientService.getWebclient().post()
.uri(oauthUrl)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData(credentials))
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse ->
Mono.error(new WebClientException(clientResponse.bodyToMono(String.class), clientResponse.statusCode())))
.bodyToMono(MicrosoftToken.class);
this.cachedToken = response.block();
The problem ist, that microsoft cannot handle a Content-type: application/x-www-form-urlencoded;charset=UTF-8.
Spring is automatically adding the charset=UTF-8 to the request. I need to get rid of this additional charset. I need a Content-Type: application/x-www-form-urlencoded. Is this possible? Otherwise i need to downgrade my spring version to 2.0.0 where the charset is not automatically be added.
My Debug Logs print:
2019-03-14 10:08:42 DEBUG [reactor.netty.channel.ChannelOperationsHandler]:
[id: 0x5d6effce, L:/192.168.148.14:52285 -
R:login.microsoftonline.de/51.4.136.42:443] Writing object
DefaultHttpRequest(decodeResult: success, version: HTTP/1.1)
POST /common/oauth2/token HTTP/1.1
user-agent: ReactorNetty/0.8.4.RELEASE
host: login.microsoftonline.de
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
Content-Length: 205
2019-03-14 10:08:42 DEBUG [reactor.netty.channel.ChannelOperationsHandler]:
[id: 0x5d6effce, L:/192.168.148.14:52285 -
R:login.microsoftonline.de/51.4.136.42:443] Writing object
I tested this with spring version 2.0.0 and there the charset is not added as in the new version:
POST /common/oauth2/token HTTP/1.1
user-agent: ReactorNetty/0.7.5.RELEASE
host: login.microsoftonline.de
accept-encoding: gzip
Content-Type: application/x-www-form-urlencoded
Content-Length: 205
This took me the best part of a morning to find out, but I finally managed. The problem is that Webflux BodyInserters.fromFormData always sets the content type to application/x-www-form-urlencoded;charset=... regardless of what you set in the headers.
To solve this, first define this method:
/**
* This method is unfortunately necessary because of Spring Webflux's propensity to add {#code ";charset=..."}
* to the {#code Content-Type} header, which the Generic Chinese Device doesn't handle properly.
*
* #return a {#link FormInserter} that doesn't add the character set to the content type header
*/
private FormInserter<String> formInserter() {
return new FormInserter<String>() {
private final MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
#Override public FormInserter<String> with(final String key, final String value) {
data.add(key, value);
return this;
}
#Override public FormInserter<String> with(final MultiValueMap<String, String> values) {
data.addAll(values);
return this;
}
#Override public Mono<Void> insert(final ClientHttpRequest outputMessage, final Context context) {
final ResolvableType formDataType =
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
return new FormHttpMessageWriter() {
#Override protected MediaType getMediaType(final MediaType mediaType) {
if (MediaType.APPLICATION_FORM_URLENCODED.equals(mediaType)) {
return mediaType;
} else {
return super.getMediaType(mediaType);
}
}
}.write(Mono.just(this.data), formDataType,
MediaType.APPLICATION_FORM_URLENCODED,
outputMessage,
context.hints());
}
};
}
Then, to call your web service, do the following:
final SomeResponseObject response = WebClient
.builder()
.build()
.post()
.uri(someOrOtherUri)
.body(formInserter().with("param1", "value1")
.with("param2", "value2")
)
.retrieve()
.bodyToFlux(SomeReponseObject.class)
.blockLast();
Please note that the block above is mainly for demonstration purposes. You may or may not want to block and wait for the response.
Here's two ways to do it:
webClient
.mutate()
.defaultHeaders(headers -> {
headers.add("Content-Type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType()
}).build()
. uri(uri)
...
OR
webClient
.post()
.uri(uri)
.body(body)
.headers(headers -> getHttpHeaders())
...
private HttpHeaders getHttpHeaders(){
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/x-www-form-urlencoded")
return headers;
}
Just a few ways you could utilize the headers consumer in .headers or .defaultHeaders..
But I don't think the charset is the issue to be honest. If you are getting application/json in your response it is probably because Microsoft is forwarding the request with that header through the redirect url you specified in your app registration.
The good news is this is probably desirable, since Microsoft returns the token fields as json, which allows you to call .bodyToMono(MicrosoftToken). I recall having issues with BodyInserters.fromFormData as it did not actually encode the values in the MultiValueMap.
This is what I'm using instead:
private BodyInserter<String, ReactiveHttpOutputMessage> getBodyInserter(Map<String,String> parameters) {
credentials.add("grant_type", encode("password"));
credentials.add("client_id", encode(oauthClientId));
credentials.add("resource", encode(oauthResource));
// and so on..
// note that parameters is a regular Map - not a MultiValueMap
BodyInserter<String, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromObject(
parameters.entrySet().stream()
.map(entry -> entry.getKey().concat("=").concat(entry.getValue()))
.collect(Collectors.joining("&", "", "")));
return bodyInserter;
}
private String encode(String str) {
try {
return URLEncoder.encode(str, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
log.error("Error encoding req body", e);
}
}

Wiremock returning wrong content-type for Spring Cloud Contract stub runner

I'm trying out Spring-Cloud-Contract for the first time. I'm trying to have my client auto-discover the contract stubs, but even though my contract specifies a content-type of 'application/json' on the response, what I get from WireMock has a content-type of 'application/octet'. What am I doing wrong?
I have a simple method in my service which returns a model like this from a /status endpoint:
{
"name": string,
"status": string
}
My contract looks like this:
import org.springframework.cloud.contract.spec.Contract
Contract.make {
request {
method('GET')
headers {
contentType(applicationJson())
}
url("/status")
}
response {
status OK()
body(
name: "Demo",
status: "RUNNING"
)
headers {
contentType(applicationJson())
}
}
}
In my client, I have a class which uses a Spring RestTemplate to query this endpoint:
#Component
public class StatusClient {
private final RestTemplate restTemplate;
public StatusClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public Status getStatus() {
return this.restTemplate
.exchange("http://localhost:8080/status", HttpMethod.GET, null, Status.class)
.getBody();
}
}
#Data
class Status implements Serializable {
private String name;
private String status;
}
My unit test uses #AutoConfigureStubRunner to pull the latest version of the contract from the local repository and assert against the response from the contract (eg name = Demo, status = RUNNING).
#RunWith(SpringRunner.class)
#SpringBootTest
#AutoConfigureStubRunner(ids = {"com.example:contract-demo:+:8080"}, stubsMode = StubRunnerProperties.StubsMode.LOCAL)
public class StatusClientTests {
#Autowired
private StatusClient client;
#Test
public void testThatStatusReturnsSuccessfully() {
Status result = this.client.getStatus();
assertEquals("Demo", result.getName());
assertEquals("RUNNING", result.getStatus());
}
}
When I run the test, WireMock reports the contract it received as expected:
2018-05-31 11:36:49.919 INFO 14212 --- [tp1255723887-26] WireMock : Request received:
127.0.0.1 - GET /status
User-Agent: [Java/1.8.0_161]
Connection: [keep-alive]
Host: [localhost:8080]
Accept: [application/json, application/*+json]
Matched response definition:
{
"status" : 200,
"body" : "{\"name\":\"Demo\",\"status\":\"RUNNING\"}",
"headers" : {
"contentType" : "application/json"
},
"transformers" : [ "response-template" ]
}
Response:
HTTP/1.1 200
contentType: [application/json]
But when the RestTemplate tries to deserialize it, it throws an exception because the response content type is actually "application/octet" once it hits the methods to extract the data:
org.springframework.web.client.RestClientException: Could not extract response: no suitable HttpMessageConverter found for response type [class com.example.contractclientdemo.Status] and content type [application/octet-stream]
at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:119)
at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:991)
at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:974)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:725)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:680)
at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:600)
at com.example.contractclientdemo.StatusClient.getStatus(StatusClient.java:18)
I'm using Finchley.RC2 for the Spring cloud version, and spring-cloud-starter-contract-stub-runner is my only test dependency other than spring-boot-starter-test.
I know WireMock is returning the wrong content type because I debugged deep into the HttpMessageConverterExtractor class in Spring and that's what the getContentType method returned when queried.
Why is WireMock returning the wrong content type, though it reports the correct one in the log? And how can I get it to properly return an application/json so I can deserialize my simple message?
I had the exact same problem than you. I solved it by adding
headers {
header 'Content-Type': 'application/json;charset=UTF-8'
}
to the response.
You seem to have in your response, although written in another way, but that did solve the problem in my case. So this have something to do with it.
Before making the change, curl showed no Content-Type response header :
curl -v -H "Accept: application/json" localhost:6565/products/ABC
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 6565 (#0)
GET /products/ABC HTTP/1.1
Host: localhost:6565
User-Agent: curl/7.58.0
Accept:application/json
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Server: Jetty(9.2.z-SNAPSHOT)
Connection #0 to host localhost left intact
{
"price": {
"currencyCode": "EUR",
"value": "100.50"
},
"name": "Fake product"
}
After doing the change, curl returned this and the RestTemplate managed to deserialize it.
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Server: Jetty(9.2.z-SNAPSHOT)
Here's my working contract :
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description "should return product information"
request{
method GET()
url("/products/ABC")
}
response {
status 200
headers {
header 'Content-Type': 'application/json;charset=UTF-8'
}
body([
name: 'Fake product',
price:[
currencyCode: 'EUR',
value: 100.50
]
])
}
}
Hope this helps
I think you should file it as an issue in WireMock. Also you're not setting the application / json content type header in the request explicit. Maybe that's a problem? Also shouldn't it be content-type as the header name in the response stub?

Issues moving from RestTemplate to WebClient (Spring Boot 2.0.0.M3)

Been stumped for a while on this one!
Moving from a regular MVC project to a reactive one, and am working with Spring Boot (new version 2.0.0.M3).
I've had zero issues with the library as a whole until this particular prblem arose.
While working with WebClient, I have a request that isn't working. It worked just fine previously with RestTemplate:
rt.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/json");
headers.add("Authorization", "Basic REDACTED");
HttpEntity<OtherApiRequest> entity =
new HttpEntity<OtherApiRequest>(CrawlRequestBuilder.buildCrawlRequest(req), headers);
ResponseEntity<Void> response = rt.postForEntity("https://other_api/path",
entity,
Void.class);
System.out.println(response.getStatusCode());
My WebClient code:
client
.post()
.uri("https://other_api/path")
.header("Authorization", "Basic REDACTED")
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(req), OtherApiRequest.class)
.exchange()
.then(res -> System.out.println(res.getStatusCode()));
I've also tried generating the body first:
ObjectMapper mapper = new ObjectMapper();
String body = mapper.writeValueAsString(
client
.post()
.uri("https://other_api/path")
.header("Authorization", "Basic REDACTED")
.contentType(MediaType.APPLICATION_JSON)
.body(body, String.class)
.exchange()
.then(res -> System.out.println(res.getStatusCode()));
Is there anything here that stands out as wrong? I can't see any issues between the two that would cause the second one to fail...
Edit:
The RestTemplate provides a response of 204. The WebClient provides a response of 400, saying the body is invalid JSON. Using the second example for WebClient above, I can print the body variable and see it is proper JSON.
Edit2: The POJO class I am serializing:
#JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class OtherApiRequest {
private String app;
private String urllist;
private int maxDepth;
private int maxUrls;
public OtherApiRequest(String app, String urllist, int maxDepth, int maxUrls) {
this.app = app;
this.urllist = urllist;
this.maxDepth = maxDepth;
this.maxUrls = maxUrls;
}
public String getApp() {
return app;
}
public String getUrllist() {
return urllist;
}
public int getMaxDepth() {
return maxDepth;
}
public int getMaxUrls() {
return maxUrls;
}
public String toString() {
return "OtherApiRequest: {" +
"app: " + app + "," +
"urllist: " + urllist + "," +
"max_depth: " + maxDepth + "," +
"max_urls: " + maxUrls +
"}";
}
}
EDIT:
See better answers here
Missing Content-Length header sending POST request with WebClient (SpringBoot 2.0.2.RELEASE)
Bug Report
https://github.com/spring-projects/spring-framework/issues/21085
Fixed in 2.2
When I experienced "Invalid JSON Response" I looked at the WebClient Request via netcat and found out, that the actual payload, in this example 3.16 was enclosed in some kind of content information:
$ netcat -l 6500
PUT /value HTTP/1.1
user-agent: ReactorNetty/0.7.5.RELEASE
transfer-encoding: chunked
host: localhost:6500
accept-encoding: gzip
Content-Type: application/json
4
3.16
0
After I added contentLength() to the builder the preceeding 4 and the trailing 0 vanished.

Resources