From 408910e893b8dd8d58afbf336dd20d211ce75425 Mon Sep 17 00:00:00 2001 From: Roy Clarkson Date: Wed, 8 Apr 2015 14:06:41 -0500 Subject: [PATCH] Ignore 'Connection: close' header from stream response This commit resolves an issue where some versions of Cloud Foundry are setting the 'Connection: close' HTTP header in the Hystrix stream response, which causes the dashboard to not display the stream data. See https://github.com/cloudfoundry/gorouter/issues/71 --- .../HystrixDashboardConfiguration.java | 46 +++++++++- .../dashboard/HystrixDashboardProperties.java | 41 +++++++++ .../HystrixDashboardConfigurationTests.java | 91 +++++++++++++++++++ 3 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 spring-cloud-netflix-hystrix-dashboard/src/main/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardProperties.java create mode 100644 spring-cloud-netflix-hystrix-dashboard/src/test/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardConfigurationTests.java diff --git a/spring-cloud-netflix-hystrix-dashboard/src/main/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardConfiguration.java b/spring-cloud-netflix-hystrix-dashboard/src/main/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardConfiguration.java index 7511f8c1..03a01983 100644 --- a/spring-cloud-netflix-hystrix-dashboard/src/main/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardConfiguration.java +++ b/spring-cloud-netflix-hystrix-dashboard/src/main/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardConfiguration.java @@ -37,10 +37,14 @@ import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.PoolingClientConnectionManager; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; + +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration; import org.springframework.boot.context.embedded.ServletRegistrationBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; import org.springframework.ui.freemarker.SpringTemplateLoader; import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; @@ -50,12 +54,16 @@ import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; */ @SuppressWarnings("deprecation") @Configuration +@EnableConfigurationProperties(HystrixDashboardProperties.class) public class HystrixDashboardConfiguration { private static final String DEFAULT_TEMPLATE_LOADER_PATH = "classpath:/templates/"; private static final String DEFAULT_CHARSET = "UTF-8"; + @Autowired + private HystrixDashboardProperties dashboardProperties; + /** * Overrides Spring Boot's {@link FreeMarkerAutoConfiguration} to prefer using a * {@link SpringTemplateLoader} instead of the file system. This corrects an issue @@ -74,7 +82,10 @@ public class HystrixDashboardConfiguration { @Bean public ServletRegistrationBean proxyStreamServlet() { - return new ServletRegistrationBean(new ProxyStreamServlet(), "/proxy.stream"); + ProxyStreamServlet proxyStreamServlet = new ProxyStreamServlet(); + proxyStreamServlet.setEnableIgnoreConnectionCloseHeader(dashboardProperties + .isEnableIgnoreConnectionCloseHeader()); + return new ServletRegistrationBean(proxyStreamServlet, "/proxy.stream"); } @Bean @@ -92,6 +103,15 @@ public class HystrixDashboardConfiguration { private static final long serialVersionUID = 1L; + private static final String CONNECTION_CLOSE_VALUE = "close"; + + private boolean enableIgnoreConnectionCloseHeader = false; + + public void setEnableIgnoreConnectionCloseHeader( + boolean enableIgnoreConnectionCloseHeader) { + this.enableIgnoreConnectionCloseHeader = enableIgnoreConnectionCloseHeader; + } + public ProxyStreamServlet() { super(); } @@ -155,9 +175,7 @@ public class HystrixDashboardConfiguration { is = httpResponse.getEntity().getContent(); // set headers - for (Header header : httpResponse.getAllHeaders()) { - response.addHeader(header.getName(), header.getValue()); - } + copyHeadersToServletResponse(httpResponse.getAllHeaders(), response); // copy data from source to response OutputStream os = response.getOutputStream(); @@ -217,6 +235,26 @@ public class HystrixDashboardConfiguration { } } } + + } + + private void copyHeadersToServletResponse(Header[] headers, + HttpServletResponse response) { + for (Header header : headers) { + // Some versions of Cloud Foundry (HAProxy) are + // incorrectly setting a "Connection: close" header + // causing the Hystrix dashboard to close the connection + // to the stream + // https://github.com/cloudfoundry/gorouter/issues/71 + if (this.enableIgnoreConnectionCloseHeader + && HttpHeaders.CONNECTION.equalsIgnoreCase(header.getName()) + && CONNECTION_CLOSE_VALUE.equalsIgnoreCase(header.getValue())) { + log.warn("Ignoring 'Connection: close' header from stream response"); + } + else { + response.addHeader(header.getName(), header.getValue()); + } + } } private static class ProxyConnectionManager { diff --git a/spring-cloud-netflix-hystrix-dashboard/src/main/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardProperties.java b/spring-cloud-netflix-hystrix-dashboard/src/main/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardProperties.java new file mode 100644 index 00000000..d088564b --- /dev/null +++ b/spring-cloud-netflix-hystrix-dashboard/src/main/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardProperties.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013-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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.cloud.netflix.hystrix.dashboard; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Roy Clarkson + */ +@ConfigurationProperties("hystrix.dashboard") +public class HystrixDashboardProperties { + + /** + * Directs the Hystrix dashboard to ignore 'Connection:close' headers if present in + * the Hystrix response stream + */ + private boolean enableIgnoreConnectionCloseHeader = false; + + public boolean isEnableIgnoreConnectionCloseHeader() { + return enableIgnoreConnectionCloseHeader; + } + + public void setEnableIgnoreConnectionCloseHeader( + boolean enableIgnoreConnectionCloseHeader) { + this.enableIgnoreConnectionCloseHeader = enableIgnoreConnectionCloseHeader; + } + +} diff --git a/spring-cloud-netflix-hystrix-dashboard/src/test/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardConfigurationTests.java b/spring-cloud-netflix-hystrix-dashboard/src/test/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardConfigurationTests.java new file mode 100644 index 00000000..f5744dcc --- /dev/null +++ b/spring-cloud-netflix-hystrix-dashboard/src/test/java/org/springframework/cloud/netflix/hystrix/dashboard/HystrixDashboardConfigurationTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2013-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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.netflix.hystrix.dashboard; + +import org.apache.http.Header; +import org.apache.http.message.BasicHeader; +import org.junit.Test; + +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; + +/** + * @author Roy Clarkson + */ +public class HystrixDashboardConfigurationTests { + + @Test + public void normal() { + MockHttpServletResponse response = new MockHttpServletResponse(); + Header[] headers = new Header[1]; + headers[0] = new BasicHeader("Content-Type", "text/proxy.stream"); + HystrixDashboardConfiguration.ProxyStreamServlet proxyStreamServlet = new HystrixDashboardConfiguration.ProxyStreamServlet(); + ReflectionTestUtils.invokeMethod(proxyStreamServlet, + "copyHeadersToServletResponse", headers, response); + assertThat(response.getHeaderNames().size(), is(1)); + assertThat(response.getHeader("Content-Type"), is("text/proxy.stream")); + } + + @Test + public void connectionClose() { + MockHttpServletResponse response = new MockHttpServletResponse(); + Header[] headers = new Header[2]; + headers[0] = new BasicHeader("Content-Type", "text/proxy.stream"); + headers[1] = new BasicHeader("Connection", "close"); + HystrixDashboardConfiguration.ProxyStreamServlet proxyStreamServlet = new HystrixDashboardConfiguration.ProxyStreamServlet(); + ReflectionTestUtils.invokeMethod(proxyStreamServlet, + "copyHeadersToServletResponse", headers, response); + assertThat(response.getHeaderNames().size(), is(2)); + assertThat(response.getHeader("Content-Type"), is("text/proxy.stream")); + assertThat(response.getHeader("Connection"), is("close")); + } + + @Test + public void ignoreConnectionClose() { + MockHttpServletResponse response = new MockHttpServletResponse(); + Header[] headers = new Header[2]; + headers[0] = new BasicHeader("Content-Type", "text/proxy.stream"); + headers[1] = new BasicHeader("Connection", "close"); + HystrixDashboardConfiguration.ProxyStreamServlet proxyStreamServlet = new HystrixDashboardConfiguration.ProxyStreamServlet(); + proxyStreamServlet.setEnableIgnoreConnectionCloseHeader(true); + ReflectionTestUtils.invokeMethod(proxyStreamServlet, + "copyHeadersToServletResponse", headers, response); + assertThat(response.getHeaderNames().size(), is(1)); + assertThat(response.getHeader("Content-Type"), is("text/proxy.stream")); + assertNull(response.getHeader("Connection")); + } + + @Test + public void doNotIgnoreConnectionClose() { + MockHttpServletResponse response = new MockHttpServletResponse(); + Header[] headers = new Header[2]; + headers[0] = new BasicHeader("Content-Type", "text/proxy.stream"); + headers[1] = new BasicHeader("Connection", "close"); + HystrixDashboardConfiguration.ProxyStreamServlet proxyStreamServlet = new HystrixDashboardConfiguration.ProxyStreamServlet(); + proxyStreamServlet.setEnableIgnoreConnectionCloseHeader(false); + ReflectionTestUtils.invokeMethod(proxyStreamServlet, + "copyHeadersToServletResponse", headers, response); + assertThat(response.getHeaderNames().size(), is(2)); + assertThat(response.getHeader("Content-Type"), is("text/proxy.stream")); + assertThat(response.getHeader("Connection"), is("close")); + } + +}