Seeing the below exception when trying to send a default constructed response for Jms inboundGateway exception from the downstream call. We are extracting the failedMessage headers from the ErrorMessage and then setting the constructed response as payload. The replyChannel headers is matching with the initially logged message header
2023-01-26 20:34:32,623 [mqGatewayListenerContainer-1] WARN o.s.m.c.GenericMessagingTemplate$TemporaryReplyChannel - be776858594e7c79 Reply message received but the receiving thread has exited due to an exception while sending the request message:
ErrorMessage [payload=org.springframework.messaging.MessageHandlingException: Failed to send or receive; nested exception is java.io.UncheckedIOException: java.net.SocketTimeoutException: Connect timed out, failedMessage=GenericMessage [payload=NOT_PRINTED, headers={replyChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel#2454562d, b3=xxxxxxxxxxxx, nativeHeaders={}, errorChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel#2454562d, sourceTransacted=false, jms_correlationId=ID:xxxxxxxxxx, id=xxxxxxxxxx, jms_expiration=36000, timestamp=1674750867614}]
Code:
return IntegrationFlows.from(Jms.inboundGateway(mqGatewayListenerContainer)
.defaultReplyQueueName(replyQueue)
.replyChannel(mqReplyChannel)
.errorChannel(appErrorChannel)
.replyTimeout(mqReplyTimeoutSeconds * 1000L))
// log
.log(DEBUG, m -> "Request Headers: " + m.getHeaders() + ", Message: " + m.getPayload())
// transform with required response headers
.transform(Message.class, m -> MessageBuilder.withPayload(m.getPayload())
.setHeader(ERROR_CHANNEL, m.getHeaders().get(ERROR_CHANNEL))
.setHeader(REPLY_CHANNEL, m.getHeaders().get(REPLY_CHANNEL))
.setHeader(CORRELATION_ID, m.getHeaders().get(MESSAGE_ID))
.setHeader(EXPIRATION, mqReplyTimeoutSeconds * 1000L)
.setHeader(MSG_HDR_SOURCE_TRANSACTED, transacted)
.build())
return IntegrationFlows.from(appErrorChannel())
.publishSubscribeChannel(
pubSubSpec -> pubSubSpec.subscribe(sf -> sf.channel(globalErrorChannel))
.<MessagingException, Message<MessagingException>>
transform(AppMessageUtil::getFailedMessageWithoutHeadersAsPayload)
.transform(p -> "Failure")
.get();
public static Message<MessagingException> getFailedMessageAsPayload(final MessagingException messagingException) {
var failedMessage = messagingException.getFailedMessage();
var failedMessageHeaders = Objects.isNull(failedMessage) ? null : failedMessage.getHeaders();
return MessageBuilder.withPayload(messagingException)
.copyHeaders(failedMessageHeaders)
.build();
}
Since you perform the processing of the request message on the same thread, it is blocked on a send and therefore we just re-throw an exception as is:
try {
doSend(channel, requestMessage, sendTimeout);
}
catch (RuntimeException ex) {
tempReplyChannel.setSendFailed(true);
throw ex;
}
And as you see we mark that tempReplyChannel as failed on a send operation.
So, the replyChannel header correlated with that mqReplyChannel is out of use. If you get rid of it at all, then everything is OK. But you also cannot reply back an Exception since the framework treats it as an error to re-throw back to the listener container:
if (errorFlowReply != null && errorFlowReply.getPayload() instanceof Throwable) {
rethrow((Throwable) errorFlowReply.getPayload(), "error flow returned an Error Message");
}
So, here is a solution:
#SpringBootApplication
public class So75249125Application {
public static void main(String[] args) {
SpringApplication.run(So75249125Application.class, args);
}
#Bean
IntegrationFlow jmsFlow(ConnectionFactory connectionFactory) {
return IntegrationFlow.from(Jms.inboundGateway(connectionFactory)
.requestDestination("testDestination")
.errorChannel("appErrorChannel"))
.transform(payload -> {
throw new RuntimeException("intentional");
})
.get();
}
#Bean
IntegrationFlow errorFlow() {
return IntegrationFlow.from("appErrorChannel")
.transform(So75249125Application::getFailedMessageAsPayload)
.get();
}
public static Message<String> getFailedMessageAsPayload(MessagingException messagingException) {
var failedMessage = messagingException.getFailedMessage();
var failedMessageHeaders = failedMessage.getHeaders();
return MessageBuilder.withPayload("failed")
.copyHeaders(failedMessageHeaders)
.build();
}
}
and unit test:
#SpringBootTest
class So75249125ApplicationTests {
#Autowired
JmsTemplate jmsTemplate;
#Test
void errorFlowRepliesCorrectly() throws JMSException {
Message reply = this.jmsTemplate.sendAndReceive("testDestination", session -> session.createTextMessage("test"));
assertThat(reply.getBody(String.class)).isEqualTo("failed");
}
}
Or even better like this:
public static String getFailedMessageAsPayload(MessagingException messagingException) {
var failedMessage = messagingException.getFailedMessage();
return "Request for '" + failedMessage.getPayload() + "' has failed";
}
and this test:
#Test
void errorFlowRepliesCorrectly() throws JMSException {
String testData = "test";
Message reply = this.jmsTemplate.sendAndReceive("testDestination", session -> session.createTextMessage(testData));
assertThat(reply.getBody(String.class)).isEqualTo("Request for '" + testData + "' has failed");
}
Related
We are using spring integration DSL to call downstream services. But we are facing issues when 4XX series error code is returned by downstream service.
Below is the code snippet that we are using to call downstream services
#Bean
public IntegrationFlow getDataChannelFlow() {
return IntegrationFlows.from(MessageChannels.executor("getDataChannel", Executors.newCachedThreadPool()))
.enrichHeaders(h -> h.headerExpression(USER_REF, SRC_USER_REF)
.handle(Http.outboundGateway(endPoint1, auth2RestTemplate)
.uriVariable("data", PAYLOAD)
.httpMethod(HttpMethod.GET)
.transferCookies(true)
.expectedResponseType(String.class), e->e.advice(integrationAdvice).id("docIDAdvice"))
.transform(this::responseTransformer)
.enrichHeaders(header -> header.headerExpression("DOCUMENTS", PAYLOAD))
}
In case of 200 and 500 response from downstream services, our code is working fine but when we get 4XX series errors we are getting below exception in logs and control does not return back to transformer method
Caused by: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:8080/fetchUser": Attempted read from closed stream.; nested exception is java.io.IOException: Attempted read from closed stream.
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:785)
at org.springframework.security.oauth2.client.OAuth2RestTemplate.doExecute(OAuth2RestTemplate.java:138)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:732)
at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:612)
at org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler.exchange(HttpRequestExecutingMessageHandler.java:196)
... 42 more
Caused by: java.io.IOException: Attempted read from closed stream.
at org.apache.http.impl.io.ChunkedInputStream.read(ChunkedInputStream.java:141)
at org.apache.http.conn.EofSensorInputStream.read(EofSensorInputStream.java:118)
Few things that we noticed while debugging -
Spring's OAuth2ErrorHandler.java class differentiates between 4XX and 5XX series of errors
public boolean hasError(ClientHttpResponse response) throws IOException
{
return HttpStatus.Series.CLIENT_ERROR.equals(response.getStatusCode().series())
|| this.errorHandler.hasError(response);
}
In above code snippet hasError() method returns true for 4XX series of codes and due to this we are getting IOException when below code snippet is executed
protected <T> T doExecute(URI url, #Nullable HttpMethod method, #Nullable RequestCallback requestCallback,
#Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {
Assert.notNull(url, "URI is required");
Assert.notNull(method, "HttpMethod is required");
ClientHttpResponse response = null;
try {
ClientHttpRequest request = createRequest(url, method);
if (requestCallback != null) {
requestCallback.doWithRequest(request);
}
response = request.execute();
handleResponse(url, method, response);
return (responseExtractor != null ? responseExtractor.extractData(response) : null);
}
catch (IOException ex) {
String resource = url.toString();
String query = url.getRawQuery();
resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
throw new ResourceAccessException("I/O error on " + method.name() +
" request for \"" + resource + "\": " + ex.getMessage(), ex);
}
finally {
if (response != null) {
response.close();
}
}
}
Our expectation is that control should return back to transformer method so that we will have the control over response processing.
Any suggestions on this issue would be much appreciated.
I'm trying to get a response from a simple protected Endpoint in the back end.
I've tested the Endpoint in Postman. I set up GET request with the KEY: Authorization and a VALUE: bearer eyxhsls...(this is the Jwt)
and the response gives me Status: 200 OK and the requested String. So everything works fine in the back end.
Now I want to replicate this process on the Client-side using Retrofit. Based on some research I using an OkHttpClient.Builder to insert the Jwt(String) into the header.
I try different things like simply inserting the Jwt(String) into header value:
Request.Builder newRequest = request.newBuilder().header("Authorization", bearerToken);
This returns a 401 status
I have also added the "Bearer " to the VALUE, just like I did in Postman, but the caller referred me to the onFailure method with the Message:
use jsonreader.setlenient(true) to accept malformed json at line 1 column 1
So I have added a GsonBuilder with setLeniet to the addConverterFactory. the caller again referred me to the onFailure method, but with the Message:
JSON document was not fully consumed.
Plz, let me know if anyone has a better idea, or understands what is going on. But stuck now for a number of days now.
public CoffeeController() {
okhttpBuilder = new OkHttpClient.Builder()
.addInterceptor(new Interceptor() {
#NonNull
#Override
public okhttp3.Response intercept(#NonNull Chain chain) throws IOException {
Request request = chain.request();
bearerToken = "Bearer " +LoginController.getToken();
bearerToken = LoginController.getToken();
Request.Builder newRequest = request.newBuilder().header("Authorization", bearerToken);
return chain.proceed(newRequest.build());
}
});
gson = new GsonBuilder()
.setLenient()
.create();
retrofit = new Retrofit.Builder()
.baseUrl("http://10.0.2.2:8080/")
.client(okhttpBuilder.build())
//.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
}
public static void CoffeeRead(Context context, TextView ResponseView) {
try {
CoffeeRepo repo = retrofit.create(CoffeeRepo.class);
Call<String> call = repo.Read();
call.enqueue(new Callback<String>() {
#Override
public void onResponse(Call<String> call, Response<String> response) {
message = "Read Coffee: " +"\nToken: " +bearerToken +"\nResponse: " + response.code();
ResponseView.setText(message);
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
}
#Override
public void onFailure(Call<String> call, Throwable t) {
message = "Failed to read coffee: \n" + t.getMessage();
ResponseView.setText(message);
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
}
});
} catch (Exception e) {
message = "Caught Exception: \n" + e.getMessage();
ResponseView.setText(message);
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
}
}
public interface CoffeeRepo {
#Headers({
"Cache-Control: max-age=3600",
"User-Agent: Android"
})
#GET("coffee")
Call<String> Read();
}
After I've added a logger, I found out the response was 200. After some research I found out I needed different ConverterFactory, instead of:
.addConverterFactory(ScalarsConverterFactory.create())
I used
.addConverterFactory(GsonConverterFactory.create(gson));
I'm using a spring cloud stream with rabbitbinder.
Using a #StreamListener, I could manually acknowledge rabbitmq messages by having Channel and deliveryTag injected into the method as follows:
#StreamListener(target = MySink.INPUT1)
public void listenForInput1(Message<String> message,
#Header(AmqpHeaders.CHANNEL) Channel channel,
#Header(AmqpHeaders.DELIVERY_TAG) Long deliveryTag) throws IOException {
log.info(" received new message [" + message.toString() + "] ");
channel.basicAck(deliveryTag, false);
}
I am now trying to achieve the same using functions:
#Bean
public Consumer<Message<String>> sink1() {
return message -> {
System.out.println("******************");
System.out.println("At Sink1");
System.out.println("******************");
System.out.println("Received message " + message.getPayload());
};
}
How do I get the Channel object in here so that I can acknowledge it with the deliveryTag?
I am able to get the delivery tag form headers. However, I am unable to get the channel Object.
I was able to figure it out:
#Bean
public Consumer<Message<String>> sink1() {
return message -> {
System.out.println("******************");
System.out.println("At Sink1");
System.out.println("******************");
System.out.println("Received message " + message.getPayload());
Channel channel = message.getHeaders().get(AmqpHeaders.CHANNEL, Channel.class);
Long deliveryTag = message.getHeaders().get(AmqpHeaders.DELIVERY_TAG, Long.class);
try {
channel.basicAck(deliveryTag, false);
} catch (IOException e) {
e.printStackTrace();
}
};
}
Have an incoming message from ActiveMQ queue and the message is being delivered properly. I need to access the JMS header value x-cutoffrule in my spring integration flow, but the value of cutoffrule in the handle section always is coming as null. My code is below:
#Bean
public JmsHeaderMapper sampleJmsHeaderMapper() {
return new DefaultJmsHeaderMapper() {
public Map<String, Object> toHeaders(javax.jms.Message jmsMessage) {
Map<String, Object> headers = super.toHeaders(jmsMessage);
try {
headers.put("cutoffrule", jmsMessage.getStringProperty("x-cutoffrule"));
} catch (JMSException e) {
e.printStackTrace();
}
return headers;
}
};
}
#Bean
public IntegrationFlow jmsMessageDrivenFlow(JmsHeaderMapper sampleJmsHeaderMapper ) {
return IntegrationFlows
.from(
Jms.messageDriverChannelAdapter(jmsMessagingTemplate.getConnectionFactory())
.destination(integrationProps.getIncomingRequestQueue())
.errorChannel(errorChannel())
.setHeaderMapper( sampleJmsHeaderMapper )
)
.handle((payload, headers) -> {
incomingPayload = payload;
logger.debug("cutoffrule"+ headers.get("cutoffrule"));
return payload;
})
.handle(message -> {
logger.debug("Message was succcessfully processed");
})
.get();
}
I thought the DefaultJmsHeaderMapper will map all JMS headers into the spring integration message. What am I missing?
The best way to understand what's wrong it to debug the code.
Or, at least log everything.
The best place for you is that your DefaultJmsHeaderMapper extension.
So, the DefaultJmsHeaderMapper maps all incoming properties. But it does that with the getObjectProperty() not getStringProperty(), like in your code:
Enumeration<?> jmsPropertyNames = jmsMessage.getPropertyNames();
if (jmsPropertyNames != null) {
while (jmsPropertyNames.hasMoreElements()) {
String propertyName = jmsPropertyNames.nextElement().toString();
try {
String headerName = this.toHeaderName(propertyName);
headers.put(headerName, jmsMessage.getObjectProperty(propertyName));
}
catch (Exception e) {
if (logger.isWarnEnabled()) {
logger.warn("error occurred while mapping JMS property '"
+ propertyName + "' to Message header", e);
}
}
}
}
So, your x-cutoffrule should be mapped exactly into the x-cutoffrule header.
See Andriy's comment, too.
When an ObjectNode is passed from the extractFramesFlow() and reaches the httpCallbackFlow(), HTTP request is successfully performed and JSON formatted payload is 'POST'ed to the "call_back" uri specified.
#Bean
public IntegrationFlow extractFramesFlow() {
return IntegrationFlows.from(extractFramesChannel())
.handle(ObjectNode.class, (payload, headers) -> {
payload = validateFields(payload);
String path = payload.get("path").asText();
try {
File moviePath = new File(path);
ArrayNode arrayNode = mapper.createArrayNode();
String imageType = payload.path("image_type").asText("JPG");
String prefix = payload.path("prefix").asText();
Tools.thumbnails(moviePath, payload.get("slice").asInt(), payload.get("scale").asInt(),
imageType, prefix, file -> arrayNode.add(file.toString()));
payload.set("files", arrayNode);
} catch (IOException e) {
e.printStackTrace();
}
return payload;
}).enrichHeaders(h-> h.header("errorChannel", "asyncErrorChannel", true))
.<ObjectNode, Boolean>route(p-> !p.hasNonNull("id"),
m->m.channelMapping("true","httpCallbackFlow.input")
.channelMapping("false","uploadToS3Channel")).get();
}
#Bean
public IntegrationFlow httpCallbackFlow() {
return f->f.handle(Http.<JsonNode>outboundChannelAdapter(m->m.getPayload().get("call_back").asText()));
}
However, when an ObjectNode is chained from the handleAsyncErrors() flow and reaches the same httpCallbackFlow(), we get an Exception which is caused by
org.springframework.web.client.RestClientException: Could not write request: no suitable HttpMessageConverter found for request type [com.fasterxml.jackson.databind.node.ObjectNode] and content type [application/x-java-serialized-object]
at org.springframework.web.client.RestTemplate$HttpEntityRequestCallback.doWithRequest(RestTemplate.java:811)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:594)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:572)
at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:493)
at org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler.handleRequestMessage(HttpRequestExecutingMessageHandler.java:382)
... 24 more
#Bean
public IntegrationFlow handleAsyncErrors() {
return IntegrationFlows.from(asyncErrorChannel())
.<MessagingException>handle((p, h) -> {
ObjectNode objectNode = mapper.createObjectNode();
objectNode.put("call_back", "http://some.test.uri");
return objectNode;
}).channel("httpCallbackFlow.input").get();
}
I don't know why we get this Exception handled by the same exact IntegrationFlow notwithstanding.
The message on the error flow has no contentType header.
It is an error message with the MessagingException payload; which has 2 properties; the cause and failedMessage.
Presumably you have a content type on the main flow message. You can set the content type with a header enricher, or add
.<MessagingException, Message<?>>transform(p -> p.getFailedMessage())
before your existing error handler, to restore the headers from the failed message.