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

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?

Related

Spring webflux: ServerResponse redirection

This is my related code:
#RestController
public class GicarController {
#PostMapping("/login")
public Mono<ServerResponse> gicar(#RequestHeader("GICAR_ID") String gicarId) {
return ServerResponse.temporaryRedirect(URI.create("/me")).build();
}
}
Issue arises when I'm calling to _/login endpoint:
$ curl -i -X POST localhost:8080/login -H "GICAR_ID: tre"
HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: text/event-stream;charset=UTF-8
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1 ; mode=block
Referrer-Policy: no-referrer
curl: (18) transfer closed with outstanding read data remaining
Why am I getting an 200 http code response?
On spring boot logging I'm getting this exception:
022-06-27 13:11:19.931 ERROR 79654 --- [or-http-epoll-2] r.n.http.server.HttpServerOperations : [9750a9d8-1, L:/127.0.0.1:8080 - R:/127.0.0.1:33150] Error finishing response. Closing connection
org.springframework.core.codec.CodecException: Type definition error: [simple type, class org.springframework.web.reactive.function.server.DefaultServerResponseBuilder$WriterFunctionResponse]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.springframework.web.reactive.function.server.DefaultServerResponseBuilder$WriterFunctionResponse and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
Why above exception is reaised?
Any ideas?
According to Spring documentation ServerResponse
Represents a typed server-side HTTP response, as returned by a handler function or filter function.
and it supposed to be used in Functional Endpoints
#Configuration
public class GicarConfiguration {
#Bean
public RouterFunction<ServerResponse> route() {
return RouterFunctions
.route(POST("/login"), this::loginHandler);
}
private Mono<ServerResponse> loginHandler(ServerRequest request) {
var gicarId = request.headers().firstHeader("GICAR_ID");
return ServerResponse.temporaryRedirect(URI.create("/me")).build();
}
}
If you still want to use Annotated Controllers, use ResponseEntity instead
#RestController
public class GicarController {
#PostMapping("/login")
public Mono<ResponseEntity<Void>> gicar() {
return Mono.just(ResponseEntity
.status(HttpStatus.TEMPORARY_REDIRECT)
.header(HttpHeaders.LOCATION, "/me")
.build()
);
}
}

Spring ControllerAdvice how to return Json or html page according to Accept request header?

Now, App can return a cutstom 404 page.
#ControllerAdvice
class GlobalControllerExceptionHandler {
#ResponseStatus(HttpStatus.NOT_FOUND)
#ExceptionHandler(NoHandlerFoundException.class)
public String handle404(WebRequest request) {
return "/exceptions/404";
}
}
And I wonder how should I do, if I want to get a JSON when Accept request header is Accept: application/json, like this:
$> curl -H "Accept: application/json" http://localhost:8080/no-such-page
{"timestamp":"2018-04-11T05:56:03.845+0000","status":404,"error":"Not Found","message":"No message available","path":"/no-such-page"}```

IllegalArgumentException: The HTTP header line does not conform to RFC 7230 when POST-accessing Spring Controller

I want to implement a user registration functionality with Spring. When I try to POST the data to the spring controller, the exception above is thrown. Surprisingly, GET Requests work on the controller.
#RestController
#RequestMapping(RestApi.ANONYMOUS + "register")
public class RegisterController {
#PostMapping(value="/new", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public UserRegistrationResultDto registerUser(#RequestBody UserRegisterDto dto) {
UserRegistrationResultDto result = registerService.registerUser(dto.getMail(),
dto.getPassword(), dto.getRepeatedPassword());
return result;
}
#GetMapping("/test")
public String test() {
return "get success";
}
}
The request that fails with error code 400 is the following:
POST http://localhost:8080/api/anonymous/register/new
Content-Type:application/x-www-form-urlencoded
{
mail=data&password=data&passwordRepeated=data
}
It throws the following exception:
java.lang.IllegalArgumentException: The HTTP header line [{: ] does not conform to RFC 7230 and has been ignored.
This get request however works:
GET http://localhost:8080/api/anonymous/register/test
I am using Tomcat 9 as a server.
Not sure how the POST request is submitted...but a space after : is required for HTTP header fields:
Content-Type: application/x-www-form-urlencoded

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

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

Spring/Eureka/Feign - FeignClient setting Content-Type header to application/x-www-form-urlencoded

When I use a FeignClient it is setting the Content-Type to application/x-www-form-urlencoded instead of application/json;charset=UTF-8.
If I use a RestTemplate to send the same message the message header Content-Type is correctly set to application/json;charset=UTF-8.
Both the FeignClient and RestTemplate are using Eureka for service discovery, and I discovered this problem by debugging the HTTP message received by the server.
The controller on the server side looks like this:
#RestController
#RequestMapping("/site/alarm")
public class SiteAlarmController {
#RequestMapping(method = RequestMethod.POST)
#ResponseBody
public ResponseEntity<RaiseAlarmResponseDto> raiseAlarm(#RequestBody RaiseSiteAlarmRequestDto requestDto) {
...
}
My FeignClient interface in the service that calls the alarm looks like this:
#FeignClient("alarm-service")
public interface AlarmFeignService {
#RequestMapping(method = RequestMethod.POST, value = "/site/alarm")
RaiseAlarmResponseDto raiseAlarm(#RequestBody RaiseSiteAlarmRequestDto requestDto);
}
The HTTP message headers from the FeignClient are:
Accept: */*
Cache-Control: no-cache
Pragma: no-cache
User-Agent: Java/1.7.0_60
Host: smit005s-MacBook-Pro.local:9120
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 323
The alarm service doesn't like the Content-Type and throws the following exception:
2015-04-22 12:12:28.580 thread="qtp1774842986-25" class="org.eclipse.jetty.servlet.ServletHandler" level="WARN"
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is feign.FeignException: status 415 reading AlarmFeignService#raiseAlarm(RaiseSiteAlarmRequestDto); content:
{"timestamp":1429701148576,"status":415,"error":"Unsupported Media Type","exception":"org.springframework.web.HttpMediaTypeNotSupportedException","message":"Unsupported Media Type","path":"/site/alarm"}
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:978) ~[spring-webmvc-4.1.5.RELEASE.jar:4.1.5.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:857) ~[spring-webmvc-4.1.5.RELEASE.jar:4.1.5.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:618) ~[tomcat-embed-core-8.0.20.jar:8.0.20]
...
... /* commented rest of stack out */
...
If I change the client side code to use a RestTemplate as follows:
#Service
public class AlarmService {
#Autowired
private RestTemplate restTemplate;
...
public void send(RaiseSiteAlarmRequestDto alarm) {
RaiseAlarmResponseDto result = restTemplate.postForObject("http://alarm-service/site/alarm",
raiseSiteAlarmRequestDto, RaiseAlarmResponseDto.class);
}
}
It works with the RestTemplate, the alarm-service receives the message and processes it successfully. The message headers sent by the RestTemplate are:
Accept: application/json, application/*+json
Content-Type: application/json;charset=UTF-8
Cache-Control: no-cache
Pragma: no-cache
User-Agent: Java/1.7.0_60
Host: smit005s-MacBook-Pro.local:9120
Connection: keep-alive
Content-Length: 323
The answer was to do as #spencergibb suggests; use the consumes directive in the #RequestMapping annotation on the FeignClient interface. This Spring/Netflix documentaition also has an example.
So for example the #FeignClient interface declaration in the client is now:
#FeignClient("alarm-service")
public interface AlarmFeignService {
#RequestMapping(method = RequestMethod.POST, value = "/site/alarm", consumes = "application/json"))
RaiseAlarmResponseDto raiseAlarm(RaiseSiteAlarmRequestDto requestDto);
}
Note this is only necessary on the client side and the server side controller does not need to have this change.
Would be nice if this was done by default on the #FeignClient and then it would be the consistent with RestTemplate and the server side controller #RequestMapping annotation. Maybe that can be done in a future release of spring-cloud.

Resources