From 9944e54ce5d9f619b1e2f9697f5fb9dfdf4eb56e Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Fri, 24 Jul 2015 13:02:58 -0400 Subject: [PATCH] INT-3781: Configure HTTP GW Timeout Status Code JIRA: https://jira.spring.io/browse/INT-3781 - Add mechanisms to set status code on an inbound gateway timeout. - Send a message to the error channel if configured. Polishing - Add failedMessage to MTE Fix Schema Docs; Timeout Detection on Error Flow Add Zookeeper Leadership Logs Fix typo in the `SmartLifecycleRoleController` --- .../gateway/MessagingGatewaySupport.java | 54 +++++++++-- .../support/SmartLifecycleRoleController.java | 20 +++- .../config/HttpInboundEndpointParser.java | 4 + .../HttpRequestHandlingEndpointSupport.java | 47 ++++++--- .../config/spring-integration-http-4.2.xsd | 23 ++++- ...pRequestHandlingMessagingGatewayTests.java | 96 ++++++++++++++++++- .../src/test/resources/log4j.properties | 1 + src/reference/asciidoc/http.adoc | 32 ++++++- src/reference/asciidoc/whats-new.adoc | 11 ++- 9 files changed, 256 insertions(+), 32 deletions(-) diff --git a/spring-integration-core/src/main/java/org/springframework/integration/gateway/MessagingGatewaySupport.java b/spring-integration-core/src/main/java/org/springframework/integration/gateway/MessagingGatewaySupport.java index b437fd9712..5b51055e5d 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/gateway/MessagingGatewaySupport.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/gateway/MessagingGatewaySupport.java @@ -16,6 +16,7 @@ package org.springframework.integration.gateway; +import org.springframework.integration.MessageTimeoutException; import org.springframework.integration.core.MessagingTemplate; import org.springframework.integration.endpoint.AbstractEndpoint; import org.springframework.integration.endpoint.EventDrivenConsumer; @@ -50,6 +51,16 @@ public abstract class MessagingGatewaySupport extends AbstractEndpoint implement private static final long DEFAULT_TIMEOUT = 1000L; + private final SimpleMessageConverter messageConverter = new SimpleMessageConverter(); + + private final MessagingTemplate messagingTemplate; + + private final HistoryWritingMessagePostProcessor historyWritingPostProcessor = + new HistoryWritingMessagePostProcessor(); + + private final Object replyMessageCorrelatorMonitor = new Object(); + + private final boolean errorOnTimeout; private volatile MessageChannel requestChannel; @@ -68,26 +79,34 @@ public abstract class MessagingGatewaySupport extends AbstractEndpoint implement @SuppressWarnings("rawtypes") private volatile InboundMessageMapper requestMapper = new DefaultRequestMapper(); - private final SimpleMessageConverter messageConverter = new SimpleMessageConverter(); - - private final MessagingTemplate messagingTemplate; - - private final HistoryWritingMessagePostProcessor historyWritingPostProcessor = - new HistoryWritingMessagePostProcessor(); - private volatile boolean initialized; private volatile AbstractEndpoint replyMessageCorrelator; - private final Object replyMessageCorrelatorMonitor = new Object(); - + /** + * Construct an instance that will return null if no reply is received. + */ public MessagingGatewaySupport() { + this(false); + } + + /** + * If errorOnTimeout is true, construct an instance that will send an + * {@link ErrorMessage} with a {@link MessageTimeoutException} payload to the error + * channel if a reply is expected but none is received. If no error channel is + * configured, the {@link MessageTimeoutException} will be thrown. + * + * @param errorOnTimeout true to create the error message. + * @since 4.2 + */ + public MessagingGatewaySupport(boolean errorOnTimeout) { MessagingTemplate template = new MessagingTemplate(); template.setMessageConverter(this.messageConverter); template.setSendTimeout(DEFAULT_TIMEOUT); template.setReceiveTimeout(this.replyTimeout); this.messagingTemplate = template; + this.errorOnTimeout = errorOnTimeout; } @@ -333,6 +352,14 @@ public abstract class MessagingGatewaySupport extends AbstractEndpoint implement error = ((ErrorMessage) reply).getPayload(); } } + if (reply == null && this.errorOnTimeout) { + if (object instanceof Message) { + error = new MessageTimeoutException((Message) object, "No reply received within timeout"); + } + else { + error = new MessageTimeoutException("No reply received within timeout"); + } + } } catch (Exception e) { if (logger.isDebugEnabled()) { @@ -363,6 +390,15 @@ public abstract class MessagingGatewaySupport extends AbstractEndpoint implement if (errorFlowReply != null && errorFlowReply.getPayload() instanceof Throwable) { this.rethrow((Throwable) errorFlowReply.getPayload(), "error flow returned an Error Message"); } + if (errorFlowReply == null && this.errorOnTimeout) { + if (object instanceof Message) { + throw new MessageTimeoutException((Message) object, + "No reply received from error channel within timeout"); + } + else { + throw new MessageTimeoutException("No reply received from error channel within timeout"); + } + } return errorFlowReply; } else { // no errorChannel so we'll propagate diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/SmartLifecycleRoleController.java b/spring-integration-core/src/main/java/org/springframework/integration/support/SmartLifecycleRoleController.java index cf5edb38f9..6cb7b75c41 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/support/SmartLifecycleRoleController.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/SmartLifecycleRoleController.java @@ -141,15 +141,23 @@ public class SmartLifecycleRoleController implements ApplicationListener> defaultMessageConverters = new ArrayList>(); + private final boolean expectReply; + private volatile List> messageConverters = new ArrayList>(); private volatile RequestMapping requestMapping = new RequestMapping(); @@ -136,8 +139,6 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa private volatile HeaderMapper headerMapper = DefaultHttpHeaderMapper.inboundMapper(); - private final boolean expectReply; - private volatile boolean extractReplyPayload = true; private volatile MultipartResolver multipartResolver; @@ -159,6 +160,7 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa } public HttpRequestHandlingEndpointSupport(boolean expectReply) { + super(expectReply); this.expectReply = expectReply; this.defaultMessageConverters.add(new MultipartAwareFormHttpMessageConverter()); this.defaultMessageConverters.add(new ByteArrayHttpMessageConverter()); @@ -331,7 +333,8 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa /** * Specify the {@link Expression} to resolve a status code for Response * to override the default '200 OK'. - *

The {@link #statusCodeExpression} is applied only for the one-way {@code }. + *

The {@link #statusCodeExpression} is applied only for the one-way {@code } + * or when no reply (timeout) is received for a gateway. * The {@code } resolves an {@link HttpStatus} from the * {@link org.springframework.integration.http.HttpHeaders#STATUS_CODE} reply {@link Message} header. * @param statusCodeExpression The status code Expression. @@ -378,11 +381,6 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa } this.validateSupportedMethods(); - if (this.expectReply && this.statusCodeExpression != null) { - logger.warn("The 'statusCodeExpression' is ignored when " + - "this component is configured as request/reply gateway"); - } - if (this.statusCodeExpression != null) { this.evaluationContext = createEvaluationContext(); } @@ -505,7 +503,23 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa Message reply = null; if (this.expectReply) { - reply = this.sendAndReceiveMessage(message); + try { + reply = this.sendAndReceiveMessage(message); + } + catch (MessageTimeoutException e) { + if (this.statusCodeExpression != null) { + reply = getMessageBuilderFactory().withPayload(e.getMessage()) + .setHeader(org.springframework.integration.http.HttpHeaders.STATUS_CODE, + evaluateHttpStatus()) + .build(); + } + else { + reply = getMessageBuilderFactory().withPayload(e.getMessage()) + .setHeader(org.springframework.integration.http.HttpHeaders.STATUS_CODE, + HttpStatus.INTERNAL_SERVER_ERROR) + .build(); + } + } } else { this.send(message); @@ -552,17 +566,22 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa protected void setStatusCodeIfNeeded(ServletServerHttpResponse response) { if (this.statusCodeExpression != null) { - if (this.evaluationContext == null) { - this.evaluationContext = createEvaluationContext(); - } - Object value = this.statusCodeExpression.getValue(this.evaluationContext); - HttpStatus httpStatus = buildHttpStatus(value); + HttpStatus httpStatus = evaluateHttpStatus(); if (httpStatus != null) { response.setStatusCode(httpStatus); } } } + private HttpStatus evaluateHttpStatus() { + if (this.evaluationContext == null) { + this.evaluationContext = createEvaluationContext(); + } + Object value = this.statusCodeExpression.getValue(this.evaluationContext); + HttpStatus httpStatus = buildHttpStatus(value); + return httpStatus; + } + /** * Prepares an instance of {@link ServletServerHttpRequest} from the raw * {@link HttpServletRequest}. Also converts the request into a multipart request to diff --git a/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-4.2.xsd b/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-4.2.xsd index 084c6951d5..8236d85c2f 100644 --- a/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-4.2.xsd +++ b/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-4.2.xsd @@ -63,12 +63,12 @@ A SpEL expression that resolves to an 'HttpStatus' code when rendering a response. - The expression must return the object which can be converted to a + The expression must return an object which can be converted to a 'org.springframework.http.HttpStatus' enum value. The 'evaluationContext' has a 'BeanResolver' but no variables, so the usage of this attribute is somewhat limited. An example might be to resolve, at runtime, some scoped Bean that returns an - 'HttpStatus' value. + 'HttpStatus' value, or use a literal expression e.g. "201". By default 'status-code-expression' is null, meaning that the default '200 OK' response status will be returned. The 'http:inbound-gateway' resolves the 'status code' from the 'http_statusCode' header of the reply @@ -203,6 +203,25 @@ ]]> + + + + A SpEL expression that resolves to an 'HttpStatus' code when rendering a response after + a 'reply-timeout'. + The expression must return an object which can be converted to a + 'org.springframework.http.HttpStatus' enum value. + The 'evaluationContext' has a 'BeanResolver' but no variables, so the usage of this attribute + is somewhat limited. + An example might be to resolve, at runtime, some scoped Bean that returns an + 'HttpStatus' value, or use a literal expression e.g. "504". + By default 'status-code-expression' is null, meaning that the default + '500 Internal Server Error' response status will be returned after a timeout. + When a timeout is not encountered, + the 'http:inbound-gateway' resolves the 'status code' from the 'http_statusCode' header of the reply + Message. + + + diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/inbound/HttpRequestHandlingMessagingGatewayTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/inbound/HttpRequestHandlingMessagingGatewayTests.java index 34f007a769..521cc58984 100644 --- a/spring-integration-http/src/test/java/org/springframework/integration/http/inbound/HttpRequestHandlingMessagingGatewayTests.java +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/inbound/HttpRequestHandlingMessagingGatewayTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package org.springframework.integration.http.inbound; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; import java.io.IOException; @@ -26,27 +27,32 @@ import java.io.PrintWriter; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import org.hamcrest.Matchers; import org.junit.Test; import org.springframework.beans.factory.BeanFactory; import org.springframework.expression.common.LiteralExpression; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.converter.AbstractHttpMessageConverter; import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; -import org.springframework.messaging.Message; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.channel.QueueChannel; import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; import org.springframework.integration.http.AbstractHttpInboundTests; +import org.springframework.integration.http.HttpHeaders; import org.springframework.integration.http.converter.SerializingHttpMessageConverter; import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.GenericMessage; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.util.LinkedMultiValueMap; @@ -296,6 +302,92 @@ public class HttpRequestHandlingMessagingGatewayTests extends AbstractHttpInboun assertEquals("text/plain", contentTypes.get(0)); } + @Test + public void timeoutDefault() throws Exception { + QueueChannel requestChannel = new QueueChannel(); + HttpRequestHandlingMessagingGateway gateway = new HttpRequestHandlingMessagingGateway(true); + gateway.setBeanFactory(mock(BeanFactory.class)); + gateway.setRequestChannel(requestChannel); + gateway.setReplyTimeout(0); + gateway.afterPropertiesSet(); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + gateway.handleRequest(request, response); + Message message = requestChannel.receive(0); + assertNotNull(message); + assertEquals(500, response.getStatus()); + } + + @Test + public void timeoutStatusExpression() throws Exception { + QueueChannel requestChannel = new QueueChannel(); + HttpRequestHandlingMessagingGateway gateway = new HttpRequestHandlingMessagingGateway(true); + gateway.setBeanFactory(mock(BeanFactory.class)); + gateway.setRequestChannel(requestChannel); + gateway.setReplyTimeout(0); + gateway.setStatusCodeExpression(new LiteralExpression("501")); + gateway.afterPropertiesSet(); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + gateway.handleRequest(request, response); + Message message = requestChannel.receive(0); + assertNotNull(message); + assertEquals(501, response.getStatus()); + } + + @Test + public void timeoutErrorFlow() throws Exception { + QueueChannel requestChannel = new QueueChannel(); + HttpRequestHandlingMessagingGateway gateway = new HttpRequestHandlingMessagingGateway(true); + gateway.setBeanFactory(mock(BeanFactory.class)); + gateway.setRequestChannel(requestChannel); + gateway.setReplyTimeout(0); + DirectChannel errorChannel = new DirectChannel(); + errorChannel.subscribe(new AbstractReplyProducingMessageHandler() { + + @Override + protected Object handleRequestMessage(Message requestMessage) { + return new GenericMessage("foo", + Collections. singletonMap(HttpHeaders.STATUS_CODE, HttpStatus.GATEWAY_TIMEOUT)); + } + + }); + gateway.setErrorChannel(errorChannel); + gateway.afterPropertiesSet(); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + gateway.handleRequest(request, response); + Message message = requestChannel.receive(0); + assertNotNull(message); + assertEquals(504, response.getStatus()); + } + + @Test + public void timeoutErrorFlowTimeout() throws Exception { + QueueChannel requestChannel = new QueueChannel(); + HttpRequestHandlingMessagingGateway gateway = new HttpRequestHandlingMessagingGateway(true); + gateway.setBeanFactory(mock(BeanFactory.class)); + gateway.setRequestChannel(requestChannel); + gateway.setReplyTimeout(0); + QueueChannel errorChannel = new QueueChannel(); + gateway.setErrorChannel(errorChannel); + gateway.setStatusCodeExpression(new LiteralExpression("501")); + gateway.afterPropertiesSet(); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + gateway.handleRequest(request, response); + Message message = requestChannel.receive(0); + assertNotNull(message); + assertEquals(501, response.getStatus()); + assertThat(response.getContentAsString(), Matchers.containsString("from error channel")); + } + + + private class ContentTypeCheckingMockHttpServletResponse extends MockHttpServletResponse { private final List contentTypeList = new ArrayList(); diff --git a/spring-integration-zookeeper/src/test/resources/log4j.properties b/spring-integration-zookeeper/src/test/resources/log4j.properties index 81c446c329..2be8933cd2 100644 --- a/spring-integration-zookeeper/src/test/resources/log4j.properties +++ b/spring-integration-zookeeper/src/test/resources/log4j.properties @@ -6,3 +6,4 @@ log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss.SSS} %-5p [%t][%c] %m log4j.category.org.springframework.integration=WARN log4j.category.org.springframework.integration.zookeeper=INFO +log4j.category.org.springframework.integration.support.SmartLifecycleRoleController=DEBUG diff --git a/src/reference/asciidoc/http.adoc b/src/reference/asciidoc/http.adoc index c88e4802a4..d8ef7dc5ec 100644 --- a/src/reference/asciidoc/http.adoc +++ b/src/reference/asciidoc/http.adoc @@ -267,7 +267,7 @@ This allows you to get the `Message` into the flow as early as possibly, e.g.: For more information regarding _Handler Mappings_, please see: http://static.springsource.org/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-handlermapping[Handler Mappings]. -[[cors]] +[[http-cors]] ==== Cross-Origin Resource Sharing (CORS) Support Starting with _version 4.2_ the `` and `` can be configured with @@ -308,12 +308,13 @@ Default value is 1800 seconds, or 30 minutes. The CORS Java Configuration is represented by the `org.springframework.integration.http.inbound.CrossOrigin` class, instances of which can be injected to the `HttpRequestHandlingEndpointSupport` beans. +[[http-response-statuscode]] ==== Response StatusCode Starting with _version 4.1_ the `` can be configured with a `status-code-expression` to override the default `200 OK` status. The expression must return an object which can be converted to an `org.springframework.http.HttpStatus` enum value. The `evaluationContext` has a `BeanResolver` but no variables, so the usage of this attribute is somewhat limited. -An example might be to resolve, at runtime, some scoped Bean that returns a status code value but, most likely, it will be set to a fixed value such as `status-code=expression="'204'"` (No Content), or `status-code-expression="T(org.springframework.http.HttpStatus).NO_CONTENT"`. +An example might be to resolve, at runtime, some scoped Bean that returns a status code value but, most likely, it will be set to a fixed value such as `status-code=expression="204"` (No Content), or `status-code-expression="T(org.springframework.http.HttpStatus).NO_CONTENT"`. By default, `status-code-expression` is null meaning that the normal '200 OK' response status will be returned. [source,xml] ---- @@ -325,6 +326,33 @@ By default, `status-code-expression` is null meaning that the normal '200 OK' re ---- The `` resolves the 'status code' from the `http_statusCode` header of the reply Message. +Starting with _version 4.2_, the default response status code when no reply is received within the `reply-timeout` +is `500 Internal Server Error`. +There are two ways to modify this behavior: + +- add a `reply-timeout-status-code-expression` - this has the same semantics as the `status-code-expression` on the +inbound adapter. +- Add an `error-channel` and return an appropriate message with an http status code header, such as... + +[source, xml] +---- + + + + + + +---- + +The payload of the `ErrorMessage` is a `MessageTimeoutException`; it must be transformed to something that can be +converted by the gateway, such as a `String`; a good candidate is the exception's message property, which is the +value used when using the expression technique. + +If the error flow times out after a main flow timeout, `500 Internal Server Error` is returned, or the +`reply-timeout-status-code-expression` is evaluated, if present. + +NOTE: previously, the default status code for a timeout was `200 OK`; to restore that behavior, set +`reply-timeout-status-code-expression="200"`. ==== URI Template Variables and Expressions diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 05e86cf00a..f99c1988ea 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -202,10 +202,19 @@ See <> for more information. [[x4.2-http-changes]] ==== HTTP Changes +===== CORS + The HTTP Inbound Endpoints (`` and ``) now allow the configuration of _Cross-Origin Resource Sharing (CORS)_. -See <> for more information. +See <> for more information. + +===== Inbound Gateway Timeout + +The HTTP inbound gateway can be configured as to what status code to return when a request times out. +The default is now `500 Internal Server Error` instead of `200 OK`. + +See <> for more information. [[x4.2-file-filter]] ==== Persistent File List Filter Changes