FeignClient throws instead of returning ResponseEntity with error http status - spring-cloud-feign

As I'm using ResponseEntity<T> as return value for my FeignClient method, I was expecting it to return a ResponseEntity with 400 status if it's what the server returns. But instead it throws a FeignException.
How can I get a proper ResponseEntity instead of an Exception from FeignClient ?
Here is my FeignClient:
#FeignClient(value = "uaa", configuration = OauthFeignClient.Conf.class)
public interface OauthFeignClient {
#RequestMapping(
value = "/oauth/token",
method = RequestMethod.POST,
consumes = MULTIPART_FORM_DATA_VALUE,
produces = APPLICATION_JSON_VALUE)
ResponseEntity<OauthTokenResponse> token(Map<String, ?> formParams);
class Conf {
#Value("${oauth.client.password}")
String oauthClientPassword;
#Bean
public Encoder feignFormEncoder() {
return new SpringFormEncoder();
}
#Bean
public Contract feignContract() {
return new SpringMvcContract();
}
#Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("web-client", oauthClientPassword);
}
}
}
and here how I use it:
#PostMapping("/login")
public ResponseEntity<LoginTokenPair> getTokens(#RequestBody #Valid LoginRequest userCredentials) {
Map<String, String> formData = new HashMap<>();
ResponseEntity<OauthTokenResponse> response = oauthFeignClient.token(formData);
//code never reached if contacted service returns a 400
...
}

By the way, solution I gave before works, but my initial intention is bad idea: an error is an error and should not be handled on nominal flow. Throwing an exception, like Feign does, and handling it with an #ExceptionHandler is a better way to go in Spring MVC world.
So two solutions:
add an #ExceptionHandler for FeignException
configure the FeignClient with an ErrorDecoder to translate the error in an Exception your business layer knows about (and already provide #ExceptionHandler for)
I prefer second solution because received error message structure is likely to change from a client to an other, so you can extract finer grained data from those error with a per-client error decoding.
FeignClient with conf (sorry for the noise introduced by feign-form)
#FeignClient(value = "uaa", configuration = OauthFeignClient.Config.class)
public interface OauthFeignClient {
#RequestMapping(
value = "/oauth/token",
method = RequestMethod.POST,
consumes = MULTIPART_FORM_DATA_VALUE,
produces = APPLICATION_JSON_VALUE)
DefaultOAuth2AccessToken token(Map<String, ?> formParams);
#Configuration
class Config {
#Value("${oauth.client.password}")
String oauthClientPassword;
#Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
#Bean
public Encoder feignFormEncoder() {
return new SpringFormEncoder(new SpringEncoder(messageConverters));
}
#Bean
public Decoder springDecoder() {
return new ResponseEntityDecoder(new SpringDecoder(messageConverters));
}
#Bean
public Contract feignContract() {
return new SpringMvcContract();
}
#Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("web-client", oauthClientPassword);
}
#Bean
public ErrorDecoder uaaErrorDecoder(Decoder decoder) {
return (methodKey, response) -> {
try {
OAuth2Exception uaaException = (OAuth2Exception) decoder.decode(response, OAuth2Exception.class);
return new SroException(
uaaException.getHttpErrorCode(),
uaaException.getOAuth2ErrorCode(),
Arrays.asList(uaaException.getSummary()));
} catch (Exception e) {
return new SroException(
response.status(),
"Authorization server responded with " + response.status() + " but failed to parse error payload",
Arrays.asList(e.getMessage()));
}
};
}
}
}
Common business exception
public class SroException extends RuntimeException implements Serializable {
public final int status;
public final List<String> errors;
public SroException(final int status, final String message, final Collection<String> errors) {
super(message);
this.status = status;
this.errors = Collections.unmodifiableList(new ArrayList<>(errors));
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SroException)) return false;
SroException sroException = (SroException) o;
return status == sroException.status &&
Objects.equals(super.getMessage(), sroException.getMessage()) &&
Objects.equals(errors, sroException.errors);
}
#Override
public int hashCode() {
return Objects.hash(status, super.getMessage(), errors);
}
}
Error handler (extracted from a ResponseEntityExceptionHandler extension)
#ExceptionHandler({SroException.class})
public ResponseEntity<Object> handleSroException(SroException ex) {
return new SroError(ex).toResponse();
}
Error response DTO
#XmlRootElement
public class SroError implements Serializable {
public final int status;
public final String message;
public final List<String> errors;
public SroError(final int status, final String message, final Collection<String> errors) {
this.status = status;
this.message = message;
this.errors = Collections.unmodifiableList(new ArrayList<>(errors));
}
public SroError(final SroException e) {
this.status = e.status;
this.message = e.getMessage();
this.errors = Collections.unmodifiableList(e.errors);
}
protected SroError() {
this.status = -1;
this.message = null;
this.errors = null;
}
public ResponseEntity<Object> toResponse() {
return new ResponseEntity(this, HttpStatus.valueOf(this.status));
}
public ResponseEntity<Object> toResponse(HttpHeaders headers) {
return new ResponseEntity(this, headers, HttpStatus.valueOf(this.status));
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SroError)) return false;
SroError sroException = (SroError) o;
return status == sroException.status &&
Objects.equals(message, sroException.message) &&
Objects.equals(errors, sroException.errors);
}
#Override
public int hashCode() {
return Objects.hash(status, message, errors);
}
}
Feign client usage notice how errors are transparently handled (no try / catch) thanks to #ControllerAdvice & #ExceptionHandler({SroException.class})
#RestController
#RequestMapping("/uaa")
public class AuthenticationController {
private static final BearerToken REVOCATION_TOKEN = new BearerToken("", 0L);
private final OauthFeignClient oauthFeignClient;
private final int refreshTokenValidity;
#Autowired
public AuthenticationController(
OauthFeignClient oauthFeignClient,
#Value("${oauth.ttl.refresh-token}") int refreshTokenValidity) {
this.oauthFeignClient = oauthFeignClient;
this.refreshTokenValidity = refreshTokenValidity;
}
#PostMapping("/login")
public ResponseEntity<LoginTokenPair> getTokens(#RequestBody #Valid LoginRequest userCredentials) {
Map<String, String> formData = new HashMap<>();
formData.put("grant_type", "password");
formData.put("client_id", "web-client");
formData.put("username", userCredentials.username);
formData.put("password", userCredentials.password);
formData.put("scope", "openid");
DefaultOAuth2AccessToken response = oauthFeignClient.token(formData);
return ResponseEntity.ok(new LoginTokenPair(
new BearerToken(response.getValue(), response.getExpiresIn()),
new BearerToken(response.getRefreshToken().getValue(), refreshTokenValidity)));
}
#PostMapping("/logout")
public ResponseEntity<LoginTokenPair> revokeTokens() {
return ResponseEntity
.ok(new LoginTokenPair(REVOCATION_TOKEN, REVOCATION_TOKEN));
}
#PostMapping("/refresh")
public ResponseEntity<BearerToken> refreshToken(#RequestHeader("refresh_token") String refresh_token) {
Map<String, String> formData = new HashMap<>();
formData.put("grant_type", "refresh_token");
formData.put("client_id", "web-client");
formData.put("refresh_token", refresh_token);
formData.put("scope", "openid");
DefaultOAuth2AccessToken response = oauthFeignClient.token(formData);
return ResponseEntity.ok(new BearerToken(response.getValue(), response.getExpiresIn()));
}
}

So, looking at source code, it seams that only solution is actually using feign.Response as return type for FeignClient methods and hand decoding the body with something like new ObjectMapper().readValue(response.body().asReader(), clazz) (with a guard on 2xx status of course because for error statuses, it's very likely that body is an error description and not a valid payload ;).
This makes possible to extract and forward status, header, body, etc. even if status is not in 2xx range.
Edit:
Here is a way to forward status, headers and mapped JSON body (if possible):
public static class JsonFeignResponseHelper {
private final ObjectMapper json = new ObjectMapper();
public <T> Optional<T> decode(Response response, Class<T> clazz) {
if(response.status() >= 200 && response.status() < 300) {
try {
return Optional.of(json.readValue(response.body().asReader(), clazz));
} catch(IOException e) {
return Optional.empty();
}
} else {
return Optional.empty();
}
}
public <T, U> ResponseEntity<U> toResponseEntity(Response response, Class<T> clazz, Function<? super T, ? extends U> mapper) {
Optional<U> payload = decode(response, clazz).map(mapper);
return new ResponseEntity(
payload.orElse(null),//didn't find a way to feed body with original content if payload is empty
convertHeaders(response.headers()),
HttpStatus.valueOf(response.status()));
}
public MultiValueMap<String, String> convertHeaders(Map<String, Collection<String>> responseHeaders) {
MultiValueMap<String, String> responseEntityHeaders = new LinkedMultiValueMap<>();
responseHeaders.entrySet().stream().forEach(e ->
responseEntityHeaders.put(e.getKey(), new ArrayList<>(e.getValue())));
return responseEntityHeaders;
}
}
that can be used as follow:
#PostMapping("/login")
public ResponseEntity<LoginTokenPair> getTokens(#RequestBody #Valid LoginRequest userCredentials) throws IOException {
Response response = oauthFeignClient.token();
return feignHelper.toResponseEntity(
response,
OauthTokenResponse.class,
oauthTokenResponse -> new LoginTokenPair(
new BearerToken(oauthTokenResponse.access_token, oauthTokenResponse.expires_in),
new BearerToken(oauthTokenResponse.refresh_token, refreshTokenValidity)));
}
This saves headers and status code, but error message is lost :/

Related

Cannot build Bitfinex Java Client - Not getting any response

So, the main goal here is to connect to the Bitfinex WebSocket by building a WebSocket Client. I would like to start receiving a stream of information(price,trades,etc). The problem is that at this stage I cannot even subscribe to a specific currency pair. In other words, I am sending a request for information to the WebSocket server but I am not receiving any responses and I cannot figure why this is. My code is below.
This is the main method
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
String URL = "wss://api-pub.bitfinex.com/ws/2/";
WebSocketClient client = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(client);
stompClient.setMessageConverter(new MappingJackson2MessageConverter());
StompSessionHandler sessionHandler = new MyStompSessionHandler();
stompClient.connect(URL,sessionHandler);
new Scanner(System.in).nextLine(); // Don't close immediately.
}
}
This is the MyStompSessionHandler
public class MyStompSessionHandler extends StompSessionHandlerAdapter{
#Override
public void afterConnected(
StompSession session, StompHeaders connectedHeaders) {
System.out.println("New session established : " + session.getSessionId());
System.out.println("wss://api-pub.bitfinex.com/ws/2");
session.send("wss://api-pub.bitfinex.com/ws/2/", getSampleMessage());
System.out.println("Message sent to websocket server");
}
#Override
public void handleException(StompSession session, StompCommand command, StompHeaders headers, byte[] payload, Throwable exception) {
System.out.println("Got an exception:" + exception);
}
#Override
public Type getPayloadType(StompHeaders headers) {
return OutboundMessage.class;
}
private Object getSampleMessage() {
InboundMessage inboundMessage = new InboundMessage();
inboundMessage.setEvent("subscribe");
inboundMessage.setChannel("ticker");
inboundMessage.setSymbol("tBTCUSD");
return inboundMessage;
}
#Override
public void handleFrame(StompHeaders headers, Object payload) {
InboundMessage msg = (InboundMessage) payload;
System.out.println(msg.toString());
}
}
This is the InboundMessage class
public class InboundMessage {
private String event;
private String channel;
private String symbol;
public InboundMessage() {
}
public InboundMessage(String event, String channel, String symbol) {
this.event = event;
this.channel = channel;
this.symbol = symbol;
}
public String getEvent() {
return event;
}
public void setEvent(String event) {
this.event = event;
}
public String getChannel() {
return channel;
}
public void setChannel(String channel) {
this.channel = channel;
}
public String getSymbol() {
return symbol;
}
public void setSymbol(String symbol) {
this.symbol = symbol;
}
#Override
public String toString() {
return "InboundMessage{" +
"event='" + event + '\'' +
", channel='" + channel + '\'' +
", symbol='" + symbol + '\'' +
'}';
}
I looked at the Bitfinex website and I don't see any evidence that STOMP is supported. They just have a REST and a WebSocket API. Therefore, using STOMP from your client isn't going to work.

RabbitListenerConfigureIntegrationTests Example

I am looking for some integration test examples for RabbitListenerConfigurer and RabbitListenerEndpointRegistrar and calling #rabbitListner annotation and test the message conversion and pass additional paramenters such as Channel and message properties etc.
Some thing like this
#RunWith(SpringJUnit4ClassRunner.class)
public class RabbitListenerConfigureIntegrationTests {
public final String sampleMessage="{\"ORCH_KEY\":{\"inputMap\":{},\"outputMap\":{\"activityId\":\"10001002\",\"activityStatus\":\"SUCCESS\"}}}";
#Test
public void testRabiitListenerConfigurer() throws Exception {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
EnableRabbitConfigWithCustomConversion.class);
RabbitListenerConfigurer registrar = ctx.getBean(RabbitListenerConfigurer.class);
/* I want to get the Listener instance here */
Message message = MessageBuilder.withBody(sampleMessage.getBytes())
.andProperties(MessagePropertiesBuilder.newInstance()
.setContentType("application/json")
.build())
.build();
/* call listener.onmessage(message) and that intern pass the call back to #rabbit listener and by that time MessageHandler which is registered should kick off and convert the message */
}
#Configuration
#EnableRabbit
public static class EnableRabbitConfigWithCustomConversion implements RabbitListenerConfigurer {
#Override
public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) {
registrar.setMessageHandlerMethodFactory(messageHandlerMethodFactory());
}
#Bean
public ConnectionFactory mockConnectionFactory() {
return mock(ConnectionFactory.class);
}
#Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(mockConnectionFactory());
factory.setAutoStartup(false);
return factory;
}
#Bean
MessageHandlerMethodFactory messageHandlerMethodFactory() {
DefaultMessageHandlerMethodFactory messageHandlerMethodFactory = new DefaultMessageHandlerMethodFactory();
messageHandlerMethodFactory.setMessageConverter(consumerJackson2MessageConverter());
return messageHandlerMethodFactory;
}
#Bean
public MappingJackson2MessageConverter consumerJackson2MessageConverter() {
return new MappingJackson2MessageConverter();
}
#Bean
public Listener messageListener1() {
return new Listener();
}
}
public class Listener {
#RabbitListener(queues = "QUEUE")
public void listen(ExchangeDTO dto, Channel chanel) {
System.out.println("Result:" + dto.getClass() + ":" + dto.toString());
/*ExchangeDTO dto = (ExchangeDTO)messageConverter.fromMessage(message);
System.out.println("dto:"+dto);*/
}
}
EDIT 2
I am not getting Exchange DTO populated with values. instead I get Null values
Here is Log :
15:00:50.994 [main] DEBUG org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter - Processing [GenericMessage [payload=byte[93], headers={contentType=application/json, id=8bf86bf1-7e45-d136-9126-69959f92f100, timestamp=1552680050993}]]
Result:class com.dsicover.dftp.scrubber.subscriber.ExchangeDTO:DTO [inputMap={}, outputMap={}]
public class ExchangeDTO implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private HashMap<String, Object> inputMap = new HashMap<String, Object>();
private HashMap<String, Object> outputMap = new HashMap<String, Object>();
public HashMap<String, Object> getInputMap() {
return inputMap;
}
public void setInputMap(HashMap<String, Object> inputMap) {
this.inputMap = inputMap;
}
public HashMap<String, Object> getOutputMap() {
return outputMap;
}
public void setOutputMap(HashMap<String, Object> outputMap) {
this.outputMap = outputMap;
}
#Override
public String toString() {
return "DTO [inputMap=" + this.inputMap + ", outputMap=" + this.outputMap + "]";
}
}
Is there any thing i am missing in Jackson2MessageConverter.
Give the #RabbitListener an id
RabbitListenerEndpointRegistry.getListenerContainer(id);
cast container to AbstractMessageListenerContainer
container.getMessageListener()
cast listener to ChannelAwareMessageListener
call onMessage().
use a mock channel and verify expected call
EDIT
#Autowired
private RabbitListenerEndpointRegistry registry;
#Test
public void test() throws Exception {
AbstractMessageListenerContainer listenerContainer =
(AbstractMessageListenerContainer) this.registry.getListenerContainer("foo");
ChannelAwareMessageListener listener =
(ChannelAwareMessageListener) listenerContainer.getMessageListener();
Channel channel = mock(Channel.class);
listener.onMessage(new Message("foo".getBytes(),
MessagePropertiesBuilder
.newInstance()
.setDeliveryTag(42L)
.build()), channel);
verify(channel).basicAck(42L, false);
}
EDIT2
Your json does not look like a DTO, it looks like a Map<String, DTO>.
This works fine for me...
#SpringBootApplication
public class So55188061Application {
public static void main(String[] args) {
SpringApplication.run(So55188061Application.class, args);
}
#RabbitListener(id = "foo", queues = "foo")
public void listen(Map<String, Foo> in, Channel channel, #Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
System.out.println(in);
channel.basicAck(tag, false);
}
#Bean
public MessageConverter converter() {
return new Jackson2JsonMessageConverter();
}
public static class Foo {
private HashMap<String, Object> inputMap = new HashMap<String, Object>();
private HashMap<String, Object> outputMap = new HashMap<String, Object>();
public HashMap<String, Object> getInputMap() {
return this.inputMap;
}
public void setInputMap(HashMap<String, Object> inputMap) {
this.inputMap = inputMap;
}
public HashMap<String, Object> getOutputMap() {
return this.outputMap;
}
public void setOutputMap(HashMap<String, Object> outputMap) {
this.outputMap = outputMap;
}
#Override
public String toString() {
return "Foo [inputMap=" + this.inputMap + ", outputMap=" + this.outputMap + "]";
}
}
}
and
#RunWith(SpringRunner.class)
#SpringBootTest
public class So55188061ApplicationTests {
public final String sampleMessage =
"{\"ORCH_KEY\":{\"inputMap\":{},"
+ "\"outputMap\":{\"activityId\":\"10001002\",\"activityStatus\":\"SUCCESS\"}}}";
#Autowired
private RabbitListenerEndpointRegistry registry;
#Test
public void test() throws Exception {
AbstractMessageListenerContainer listenerContainer = (AbstractMessageListenerContainer) this.registry
.getListenerContainer("foo");
ChannelAwareMessageListener listener = (ChannelAwareMessageListener) listenerContainer.getMessageListener();
Channel channel = mock(Channel.class);
listener.onMessage(MessageBuilder.withBody(sampleMessage.getBytes())
.andProperties(MessagePropertiesBuilder.newInstance()
.setContentType("application/json")
.setDeliveryTag(42L)
.build())
.build(),
channel);
verify(channel).basicAck(42L, false);
}
}
and
{ORCH_KEY=Foo [inputMap={}, outputMap={activityId=10001002, activityStatus=SUCCESS}]}
According to your complex requirement to have everything on board, I don't see how we can make a deal with the mock(ConnectionFactory.class). We would need to mock much more to have everything working.
Instead, I would suggest to take a look into the real integration test against existing RabbitMQ or at least embedded QPid.
In addition you may consider to use a #RabbitListenerTest to spy your #RabbitListener invocation without interfering your production code.
More info is in the Reference Manual: https://docs.spring.io/spring-amqp/docs/2.1.4.RELEASE/reference/#test-harness

Feign ErrorDecoder : retrieve the original message

I use a ErrorDecoder to return the right exception rather than a 500 status code.
Is there a way to retrieve the original message inside the decoder. I can see that it is inside the FeignException, but not in the decode method. All I have is the 'status code' and a empty 'reason'.
public class CustomErrorDecoder implements ErrorDecoder {
private final ErrorDecoder errorDecoder = new Default();
#Override
public Exception decode(String s, Response response) {
switch (response.status()) {
case 404:
return new FileNotFoundException("File no found");
case 403:
return new ForbiddenAccessException("Forbidden access");
}
return errorDecoder.decode(s, response);
}
}
Here the original message : "message":"Access to the file forbidden"
feign.FeignException: status 403 reading ProxyMicroserviceFiles#getUserRoot(); content:
{"timestamp":"2018-11-28T17:34:05.235+0000","status":403,"error":"Forbidden","message":"Access to the file forbidden","path":"/root"}
Also I use my FeignClient interface like a RestController so I don't use any other Controler populated with the proxy that could encapsulate the methods calls.
#RestController
#FeignClient(name = "zuul-server")
#RibbonClient(name = "microservice-files")
public interface ProxyMicroserviceFiles {
#GetMapping(value = "microservice-files/root")
Object getUserRoot();
#GetMapping(value = "microservice-files/file/{id}")
Object getFileById(#PathVariable("id") int id);
}
Here is a solution, the message is actually in the response body as a stream.
package com.clientui.exceptions;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.CharStreams;
import feign.Response;
import feign.codec.ErrorDecoder;
import lombok.*;
import java.io.*;
public class CustomErrorDecoder implements ErrorDecoder {
private final ErrorDecoder errorDecoder = new Default();
#Override
public Exception decode(String s, Response response) {
String message = null;
Reader reader = null;
try {
reader = response.body().asReader();
//Easy way to read the stream and get a String object
String result = CharStreams.toString(reader);
//use a Jackson ObjectMapper to convert the Json String into a
//Pojo
ObjectMapper mapper = new ObjectMapper();
//just in case you missed an attribute in the Pojo
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
//init the Pojo
ExceptionMessage exceptionMessage = mapper.readValue(result,
ExceptionMessage.class);
message = exceptionMessage.message;
} catch (IOException e) {
e.printStackTrace();
}finally {
//It is the responsibility of the caller to close the stream.
try {
if (reader != null)
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
switch (response.status()) {
case 404:
return new FileNotFoundException(message == null ? "File no found" :
message);
case 403:
return new ForbiddenAccessException(message == null ? "Forbidden
access" : message);
}
return errorDecoder.decode(s, response);
}
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#ToString
public static class ExceptionMessage{
private String timestamp;
private int status;
private String error;
private String message;
private String path;
}
}
If you want to get the response payload body, with the Feign exception, just use this method:
feignException.contentUTF8();
Example:
try {
itemResponse = call(); //method with the feign call
} catch (FeignException e) {
logger.error("ResponseBody: " + e.contentUTF8());
}
It is suggested to use input stream instead of reader and map it to your object.
package com.clientui.exceptions;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.CharStreams;
import feign.Response;
import feign.codec.ErrorDecoder;
import lombok.*;
import java.io.*;
public class CustomErrorDecoder implements ErrorDecoder {
private final ErrorDecoder errorDecoder = new Default();
#Override
public Exception decode(String s, Response response) {
String message = null;
InputStream responseBodyIs = null;
try {
responseBodyIs = response.body().asInputStream();
ObjectMapper mapper = new ObjectMapper();
ExceptionMessage exceptionMessage = mapper.readValue(responseBodyIs, ExceptionMessage.class);
message = exceptionMessage.message;
} catch (IOException e) {
e.printStackTrace();
// you could also return an exception
return new errorMessageFormatException(e.getMessage());
}finally {
//It is the responsibility of the caller to close the stream.
try {
if (responseBodyIs != null)
responseBodyIs.close();
} catch (IOException e) {
e.printStackTrace();
}
}
switch (response.status()) {
case 404:
return new FileNotFoundException(message == null ? "File no found" :
message);
case 403:
return new ForbiddenAccessException(message == null ? "Forbidden access" : message);
}
return errorDecoder.decode(s, response);
}
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#ToString
public static class ExceptionMessage{
private String timestamp;
private int status;
private String error;
private String message;
private String path;
}
}
If you're like me and really just want the content out of a failed Feign call without all these custom decoders and boilerplate, there is a hacky way do this.
If we look at FeignException when it is being created and a response body exists, it assembles the exception message like so:
if (response.body() != null) {
String body = Util.toString(response.body().asReader());
message += "; content:\n" + body;
}
Therefore if you're after the response body, you can just pull it out by parsing the Exception message since it is delimited by a newline.
String[] feignExceptionMessageParts = e.getMessage().split("\n");
String responseContent = feignExceptionMessageParts[1];
And if you want the object, you can use something like Jackson:
MyResponseBodyPojo errorBody = objectMapper.readValue(responseContent, MyResponseBodyPojo.class);
I do not claim this is a smart approach or a best practice.
The original message is within the Response body, as already answered. However, we can reduce the amount of boilerplate using Java 8 Streams to read it:
public class CustomErrorDecoder implements ErrorDecoder {
private final ErrorDecoder errorDecoder = new Default();
#Override
public Exception decode(String s, Response response) {
String body = "4xx client error";
try {
body = new BufferedReader(response.body().asReader(StandardCharsets.UTF_8))
.lines()
.collect(Collectors.joining("\n"));
} catch (IOException ignore) {}
switch (response.status()) {
case 404:
return new FileNotFoundException(body);
case 403:
return new ForbiddenAccessException(body);
}
return errorDecoder.decode(s, response);
}
}
Some refactoring and code style on accepted answer:
#Override
#SneakyThrows
public Exception decode(String methodKey, Response response) {
String message;
try (Reader reader = response.body().asReader()) {
String result = StringUtils.toString(reader);
message = mapper.readValue(result, ErrorResponse.class).getMessage();
}
if (response.status() == 401) {
return new UnauthorizedException(message == null ? response.reason() : message);
}
if (response.status() == 403) {
return new ForbiddenException(message == null ? response.reason() : message);
}
return defaultErrorDecoder.decode(methodKey, response);
}

Default to content_type application/json with overriden isFatal from DefaultExceptionStrategy

I'd like to not require my clients to provide content_type application/json but just default to it. I got this working.
I also tried to combine with another example to implement a custom isFatal(Throwable t) from ConditionalRejectingErrorHandler. I can get my custom error handler to fire, but then it seems to require the content_type property again. I can't figure out how to get them both to work at the same time.
Any ideas?
The below successfully works to not require content_type
EDIT: The below code does not work as I thought. An old message in the queue with the property content_type application/json must have been pulled in
#EnableRabbit
#Configuration
public class ExampleRabbitConfigurer implements
RabbitListenerConfigurer {
#Value("${spring.rabbitmq.host:'localhost'}")
private String host;
#Value("${spring.rabbitmq.port:5672}")
private int port;
#Value("${spring.rabbitmq.username}")
private String username;
#Value("${spring.rabbitmq.password}")
private String password;
#Autowired
private MappingJackson2MessageConverter mappingJackson2MessageConverter;
#Autowired
private DefaultMessageHandlerMethodFactory messageHandlerMethodFactory;
#Bean
public MappingJackson2MessageConverter mappingJackson2MessageConverter() {
return new MappingJackson2MessageConverter();
}
#Bean
public DefaultMessageHandlerMethodFactory messageHandlerMethodFactory() {
DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
factory.setMessageConverter(mappingJackson2MessageConverter);
return factory;
}
#Override
public void configureRabbitListeners(final RabbitListenerEndpointRegistrar registrar) {
registrar.setMessageHandlerMethodFactory(messageHandlerMethodFactory);
}
The below here works to override isFatal() in ConditionalRejectingErrorHandler. The SimpleRabbitListenerContainerFactory.setMessageConverter() seems like it should serve the same purpose as DefaultMessageHandlerMethodFactory.setMessageConverter(). Obviously this is not the case.
#EnableRabbit
#Configuration
public class ExampleRabbitConfigurer {
#Value("${spring.rabbitmq.host:'localhost'}")
private String host;
#Value("${spring.rabbitmq.port:5672}")
private int port;
#Value("${spring.rabbitmq.username}")
private String username;
#Value("${spring.rabbitmq.password}")
private String password;
#Autowired
ConnectionFactory connectionFactory;
#Autowired
Jackson2JsonMessageConverter jackson2JsonConverter;
#Autowired
ErrorHandler amqpErrorHandlingExceptionStrategy;
#Bean
public Jackson2JsonMessageConverter jackson2JsonConverter() {
return new Jackson2JsonMessageConverter();
}
#Bean
public ErrorHandler amqpErrorHandlingExceptionStrategy() {
return new ConditionalRejectingErrorHandler(new AmqpErrorHandlingExceptionStrategy());
}
#Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(jackson2JsonConverter);
factory.setErrorHandler(amqpErrorHandlingExceptionStrategy);
return factory;
}
public static class AmqpErrorHandlingExceptionStrategy extends ConditionalRejectingErrorHandler.DefaultExceptionStrategy {
private final Logger LOGGER = org.slf4j.LoggerFactory.getLogger(getClass());
#Override
public boolean isFatal(Throwable t) {
if (t instanceof ListenerExecutionFailedException) {
ListenerExecutionFailedException lefe = (ListenerExecutionFailedException) t;
LOGGER.error("Failed to process inbound message from queue "
+ lefe.getFailedMessage().getMessageProperties().getConsumerQueue()
+ "; failed message: " + lefe.getFailedMessage(), t);
}
return super.isFatal(t);
}
}
Use an "after receive" MessagePostProcessor to add the contentType header to the inbound message.
Starting with version 2.0, you can add the MPP to the container factory.
For earlier versions you can reconfigure...
#SpringBootApplication
public class So47424449Application {
public static void main(String[] args) {
SpringApplication.run(So47424449Application.class, args);
}
#Bean
public ApplicationRunner runner(RabbitListenerEndpointRegistry registry, RabbitTemplate template) {
return args -> {
SimpleMessageListenerContainer container =
(SimpleMessageListenerContainer) registry.getListenerContainer("myListener");
container.setAfterReceivePostProcessors(m -> {
m.getMessageProperties().setContentType("application/json");
return m;
});
container.start();
// send a message with no content type
template.setMessageConverter(new SimpleMessageConverter());
template.convertAndSend("foo", "{\"bar\":\"baz\"}", m -> {
m.getMessageProperties().setContentType(null);
return m;
});
template.convertAndSend("foo", "{\"bar\":\"qux\"}", m -> {
m.getMessageProperties().setContentType(null);
return m;
});
};
}
#Bean
public Jackson2JsonMessageConverter converter() {
return new Jackson2JsonMessageConverter();
}
#RabbitListener(id = "myListener", queues = "foo", autoStartup = "false")
public void listen(Foo foo) {
System.out.println(foo);
if (foo.bar.equals("qux")) {
throw new MessageConversionException("test");
}
}
public static class Foo {
public String bar;
public String getBar() {
return this.bar;
}
public void setBar(String bar) {
this.bar = bar;
}
#Override
public String toString() {
return "Foo [bar=" + this.bar + "]";
}
}
}
As you can see, since it modifies the source message, the modified header is available in the error handler...
2017-11-22 09:39:26.615 WARN 97368 --- [cTaskExecutor-1] ingErrorHandler$DefaultExceptionStrategy : Fatal message conversion error; message rejected; it will be dropped or routed to a dead letter exchange, if so configured: (Body:'{"bar":"qux"}' MessageProperties [headers={}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=, receivedRoutingKey=foo, deliveryTag=2, consumerTag=amq.ctag-re1kcxKV14L_nl186stM0w, consumerQueue=foo]), contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=, receivedRoutingKey=foo, deliveryTag=2, consumerTag=amq.ctag-re1kcxKV14L_nl186stM0w, consumerQueue=foo])

Spring websocket messaging template isn't publishing data from Controller without #Scheduler annotation

#Controller
public class WebServiceController {
private static Logger logger = LoggerFactory.getLogger(WebServiceController.class.getName());
#Autowired
private SimpMessagingTemplate template;
public static Response response;
private final MessageSendingOperations<String> messagingTemplate;
#Autowired
private XmlCommandFactory xmlCommandFactory;
#Autowired
public WebServiceController(final MessageSendingOperations<String> messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
#RequestMapping(value = "/handleResponse.htm/{sessionId}")
public void handleRequest(final HttpServletRequest request, final HttpServletResponse httpResponse,
#PathVariable final String sessionId) throws Exception {
httpResponse.setStatus(200);
final StringBuilder buffer = new StringBuilder();
final BufferedReader reader = request.getReader();
String line = null;
while ((line = reader.readLine()) != null) {
buffer.append(line.trim());
}
if (!buffer.toString().isEmpty()) {
try {
response = xmlCommandFactory.createCommand(buffer.toString(), sessionId);
publishData();
} catch (final Exception ex) {
logger.warn(" Error while processing response ", ex);
}
}
}
#Scheduled(fixedDelay = 2000)
public void publishData() {
try {
if (null != response && !response.getData().isEmpty()) {
this.messagingTemplate.convertAndSend("/update/config/" + response.getSessionId(), response);
}
} catch (final Exception e) {
logger.warn("exception " + e);
}
}
The above code publishes data to the UI at the scheduled delay. But if I comment out #Scheduler annotation and call this method from handleRequest, it doesn't publish data to UI. What could be the reason?

Resources