Spring Integration DSL - OAuth2ErrorHandler issues with 4XX series error codes - spring-integration

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.

Related

Spring Integration Default Response for Jms inboundGateway

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");
}

Error handling - no output-channel or replyChannel header available

I am trying to handle exceptions using ExpressionEvaluatingRequestHandlerAdvice, have a transformer for a fail channel,
<int:transformer input-channel="afterFailureChannel" output-channel="validateOutputChannel" ref="testExceptionTransformer" method="handleLockServiceResponse"/>
In testExceptionTransformer, I am forming user defined exception and sending it in http response entity which I want to send as a rest api response, Even though transformer has outputChannel, application throws
org.springframework.messaging.core.DestinationResolutionException: no output-channel or replyChannel header available
at org.springframework.integration.handler.AbstractMessageProducingHandler.sendOutput(AbstractMessageProducingHandler.java:452) ~[spring-integration-core-5.5.13.jar:5.5.13]
Could you please help?
Edit:
Transformer looks like this,
public ResponseEntity<Object> handleLockServiceResponse(Message<MessagingException> message) throws Exception {
ResponseEntity<Object> response = null;
LOGGER.error(message.getPayload().getFailedMessage().toString());
LOGGER.error(message.getPayload().getCause().toString());
try {
Throwable exception = message.getPayload().getCause();
if (exception.getCause() instanceof HttpClientErrorException) {
throw new handleValidationException(exception.getCause().getMessage());
}
}catch(handleValidationException ex){
return adapterErrorHandler.handleCustomValidationException(ex);
}
return response;
}
It indeed doesn't fail in your transformer since you have that output-channel it fails in the initial gateway when it tries to correlate the reply message into a TemporaryReplyChannel from headers. We need to see what your transformer does, but the rule of thumb is if you return a Message from the transformer, you have to coyp headers from request message. However with an ExpressionEvaluatingRequestHandlerAdvice and its failureChannel it is a bit tricky.
The logic there is like this:
if (evalResult != null && this.failureChannel != null) {
MessagingException messagingException =
new MessageHandlingExpressionEvaluatingAdviceException(message, "Handler Failed",
unwrapThrowableIfNecessary(exception), evalResult);
ErrorMessage errorMessage = new ErrorMessage(messagingException);
this.messagingTemplate.send(this.failureChannel, errorMessage);
}
It becomes obvious that ErrorMessage doesn't have a request message headers. So, you need to extract them from that exception via getFailedMessage() and that's the one is sent to your service instrumented with that ExpressionEvaluatingRequestHandlerAdvice.
We probably need to improve the doc on the matter: https://docs.spring.io/spring-integration/docs/current/reference/html/messaging-endpoints.html#expression-advice
UPDATE
So, now you return a ResponseEntity from your transformer method and headers for the reply message is copied from that ErrorMessage we send from the ExpressionEvaluatingRequestHandlerAdvice. To preserve original message headers in the reply message you must do something like this:
public Message<ResponseEntity<Object>> handleLockServiceResponse(Message<MessagingException> message) throws Exception {
ResponseEntity<Object> response = null;
LOGGER.error(message.getPayload().getFailedMessage().toString());
LOGGER.error(message.getPayload().getCause().toString());
try {
Throwable exception = message.getPayload().getCause();
if (exception.getCause() instanceof HttpClientErrorException) {
throw new handleValidationException(exception.getCause().getMessage());
}
}catch(handleValidationException ex){
response = adapterErrorHandler.handleCustomValidationException(ex);
}
return MessageBuilder.withPayload(response).copyHeaders(message.getPayload().getFailedMessage().getHeaders()).build();
}

Trouble sending the Jwt in the request header

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));

Exception thrown from ExpressionEvaluatingRequestHandlerAdvice triggers error handler handler on Adapter

I set an advice on my `MessageHandler'
#ServiceActivator(inputChannel = "outbound",adviceChain = "expressionAdvice")
public MessageHandler...
and configured it as:
#Bean
public ExpressionEvaluatingRequestHandlerAdvicer expressionAdvice() {
ExpressionEvaluatingRequestHandlerAdvice advice = new ExpressionEvaluatingRequestHandlerAdvice();
advice.setFailureChannelName("failure");
return advice;
}
in failure handler I parse and detect the errors
#ServiceActivator(inputChannel = "failure")
public void handleFailures(Message<?> message) {
ExpressionEvaluatingRequestHandlerAdvice.MessageHandlingExpressionEvaluatingAdviceException adviceException = (ExpressionEvaluatingRequestHandlerAdvice.MessageHandlingExpressionEvaluatingAdviceException) message.getPayload();
Throwable cause = adviceException.getCause().getCause().getCause();
for specific errors I am doing some operations and flow is resumed.
But for specific error type I just log the error and continue, for other types I am rethrowing exception to get a retry.
This works, but there is a side affect, this throw Exception triggers ServiceActivator that was set on MessageProducerSupport.setErrorChannelName on the adapter.
#ServiceActivator(inputChannel = "onerror")
It does the job but I would like to avoid calling it, just to do the retries without going to this handler.
I do need this handler to catch other types of errors coming from source-channel.
See this option on that ExpressionEvaluatingRequestHandlerAdvice:
/**
* If true, any exception will be caught and null returned.
* Default false.
* #param trapException true to trap Exceptions.
*/
public void setTrapException(boolean trapException) {
More info in docs: https://docs.spring.io/spring-integration/docs/current/reference/html/messaging-endpoints.html#message-handler-advice-chain
UPDATE
For conditional exceptions "trapping", you need consider to implement a logic in your failure channel sub-flow. However trapException is still must be true.
This is the logic we have so far in the ExpressionEvaluatingRequestHandlerAdvice:
protected Object doInvoke(ExecutionCallback callback, Object target, Message<?> message) {
try {
Object result = callback.execute();
if (this.onSuccessExpression != null) {
evaluateSuccessExpression(message);
}
return result;
}
catch (RuntimeException e) {
Exception actualException = unwrapExceptionIfNecessary(e);
if (this.onFailureExpression != null) {
Object evalResult = evaluateFailureExpression(message, actualException);
if (this.returnFailureExpressionResult) {
return evalResult;
}
}
if (!this.trapException) {
if (e instanceof ThrowableHolderException) { // NOSONAR
throw (ThrowableHolderException) e;
}
else {
throw new ThrowableHolderException(actualException); // NOSONAR lost stack trace
}
}
return null;
}
}
So, we catch an exception for a callback.execute() and process it in the evaluateFailureExpression() (which may just send an ErrorMessage to the mentioned failureChannel). Such a this.messagingTemplate.send(this.failureChannel, errorMessage); is not wrapped into a try..catch, so if you re-throw an exception from your error handling flow, it is going to be bubbled to the main flow.

Spring Integration Java DSL and Http.outboundGateway: How to get the real error message JSON

How to get the real error message JSON when the Http.outboundGateway call is failed.
For example my program does the HTTP POST. The operation fails with the error code 400 Bad Request and the real error message is (tested with the Postman):
{
"name": [
"This field is needed."
]
}
I have the error channel like this:
#Bean
private IntegrationFlow myErrorChannel() {
return f -> f.handle("myErrorHandler", "handle")
....
;
}
and the Class MyErrorHandler is like this:
#Service
public class MyErrorHandler {
#ServiceActivator
public Message<MessageHandlingException> handle(Message<MessageHandlingException> message) {
...
}
}
Does the MessageHandlingException contain the real error message?
{
"name": [
"This field is needed."
]
}
I debugged the code and checked the MessageHandlingException exception and it seems it doesn't contain the real error message. The detailMessage contains the text 400 Bad Request, but I want to know the real error message.
How to get the real error message?
Edit:
This is working (I'm assigning the real error message to the new payload):
final RestClientResponseException clientException = (RestClientResponseException) messagingHandlingException.getCause();
payload = clientException.getResponseBodyAsString();
The Resttemplate uses a DefaultResponseErrorHandler by default. That one has a logic like:
protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
String statusText = response.getStatusText();
HttpHeaders headers = response.getHeaders();
byte[] body = getResponseBody(response);
Charset charset = getCharset(response);
switch (statusCode.series()) {
case CLIENT_ERROR:
throw HttpClientErrorException.create(statusCode, statusText, headers, body, charset);
case SERVER_ERROR:
throw HttpServerErrorException.create(statusCode, statusText, headers, body, charset);
default:
throw new UnknownHttpStatusCodeException(statusCode.value(), statusText, headers, body, charset);
}
}
An exception from here is thrown to the HttpRequestExecutingMessageHandler which really wraps it into the MessageHandlingException.
Since you say that you can handle the last one via your MyErrorHandler, I would suggest you just take a look into the cause of the MessageHandlingException, and you'll that RestClientResponseException with all the required info from the response.

Resources