From d52f4605626a34dc8c488dfa3e62eed605053382 Mon Sep 17 00:00:00 2001 From: Diwaker Gupta Date: Wed, 10 Jul 2013 14:30:51 -0700 Subject: [PATCH] Introduce StripExpectHeader filter and a property to control it. Some providers (specifically HP Cloud and Google Cloud Storage) do not properly support Expect: 100-continue headers. JDK7 is stricter in its handling of the Expect header than JDK6 -- in particular, it expects servers to properly respond to an expect header and times out only if a prior timeout did not exist on the underlying HTTP connection. As a result, JDK7 tests against these providers hang and fail. This commit introduces a new filter -- appropriate called StripExpectHeader -- that is controlled by the property jclouds.strip-expect-header. The property defaults to false to preserve existing behavior but allows applications to tweak Expect header handling. Tested by running HPCS live tests with JDK7 -- previously most of these tests would fail with timeouts. Closes JCLOUDS-181 --- core/src/main/java/org/jclouds/Constants.java | 5 +++ .../apis/internal/BaseApiMetadata.java | 3 +- .../http/filters/StripExpectHeader.java | 36 ++++++++++++++++ .../internal/RestAnnotationProcessor.java | 10 ++++- .../http/filters/StripExpectHeaderTest.java | 38 ++++++++++++++++ .../internal/RestAnnotationProcessorTest.java | 43 +++++++++++++++++-- 6 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 core/src/main/java/org/jclouds/http/filters/StripExpectHeader.java create mode 100644 core/src/test/java/org/jclouds/http/filters/StripExpectHeaderTest.java diff --git a/core/src/main/java/org/jclouds/Constants.java b/core/src/main/java/org/jclouds/Constants.java index 1185edbfe1..2c4304de9d 100644 --- a/core/src/main/java/org/jclouds/Constants.java +++ b/core/src/main/java/org/jclouds/Constants.java @@ -296,4 +296,9 @@ public interface Constants { */ public static final String PROPERTY_PRETTY_PRINT_PAYLOADS = "jclouds.payloads.pretty-print"; + /** + * When true, strip the Expect: 100-continue header. Useful when interacting with + * providers that don't properly support Expect headers. Defaults to false. + */ + public static final String PROPERTY_STRIP_EXPECT_HEADER = "jclouds.strip-expect-header"; } diff --git a/core/src/main/java/org/jclouds/apis/internal/BaseApiMetadata.java b/core/src/main/java/org/jclouds/apis/internal/BaseApiMetadata.java index 42b159ae84..66a9110615 100644 --- a/core/src/main/java/org/jclouds/apis/internal/BaseApiMetadata.java +++ b/core/src/main/java/org/jclouds/apis/internal/BaseApiMetadata.java @@ -29,6 +29,7 @@ import static org.jclouds.Constants.PROPERTY_PRETTY_PRINT_PAYLOADS; import static org.jclouds.Constants.PROPERTY_SCHEDULER_THREADS; import static org.jclouds.Constants.PROPERTY_SESSION_INTERVAL; import static org.jclouds.Constants.PROPERTY_SO_TIMEOUT; +import static org.jclouds.Constants.PROPERTY_STRIP_EXPECT_HEADER; import static org.jclouds.Constants.PROPERTY_USER_THREADS; import static org.jclouds.reflect.Reflection2.typeToken; @@ -46,7 +47,6 @@ import com.google.common.base.Optional; import com.google.common.collect.ImmutableSet; import com.google.common.reflect.TypeToken; import com.google.inject.Module; - /** * The BaseApiMetadata class is an abstraction of {@link ApiMetadata} to be extended by those * implementing ApiMetadata. @@ -72,6 +72,7 @@ public abstract class BaseApiMetadata implements ApiMetadata { props.setProperty(PROPERTY_MAX_SESSION_FAILURES, 2 + ""); props.setProperty(PROPERTY_SESSION_INTERVAL, 60 + ""); props.setProperty(PROPERTY_PRETTY_PRINT_PAYLOADS, "true"); + props.setProperty(PROPERTY_STRIP_EXPECT_HEADER, "false"); return props; } diff --git a/core/src/main/java/org/jclouds/http/filters/StripExpectHeader.java b/core/src/main/java/org/jclouds/http/filters/StripExpectHeader.java new file mode 100644 index 0000000000..fa8c7d751a --- /dev/null +++ b/core/src/main/java/org/jclouds/http/filters/StripExpectHeader.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.jclouds.http.filters; + +import org.jclouds.http.HttpException; +import org.jclouds.http.HttpRequest; +import org.jclouds.http.HttpRequestFilter; + +import com.google.common.net.HttpHeaders; +import com.google.inject.Singleton; + +/** + * @author Diwaker Gupta + */ +@Singleton +public class StripExpectHeader implements HttpRequestFilter { + @Override + public HttpRequest filter(HttpRequest request) throws HttpException { + return request.toBuilder().removeHeader(HttpHeaders.EXPECT).build(); + } +} diff --git a/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java b/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java index 2a32ad1feb..7fd789dcb9 100644 --- a/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java +++ b/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java @@ -50,7 +50,6 @@ import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Set; - import javax.annotation.Resource; import javax.inject.Named; import javax.ws.rs.FormParam; @@ -65,6 +64,7 @@ import org.jclouds.http.HttpRequest; import org.jclouds.http.HttpRequestFilter; import org.jclouds.http.HttpUtils; import org.jclouds.http.Uris.UriBuilder; +import org.jclouds.http.filters.StripExpectHeader; import org.jclouds.http.options.HttpRequestOptions; import org.jclouds.io.ContentMetadataCodec; import org.jclouds.io.Payload; @@ -149,11 +149,13 @@ public class RestAnnotationProcessor implements Function tokenValues = LinkedHashMultimap.create(); diff --git a/core/src/test/java/org/jclouds/http/filters/StripExpectHeaderTest.java b/core/src/test/java/org/jclouds/http/filters/StripExpectHeaderTest.java new file mode 100644 index 0000000000..c38002cac5 --- /dev/null +++ b/core/src/test/java/org/jclouds/http/filters/StripExpectHeaderTest.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.jclouds.http.filters; + +import static org.testng.Assert.assertFalse; + +import org.jclouds.http.HttpRequest; +import org.testng.annotations.Test; + +import com.google.common.net.HttpHeaders; + +/** + * @author Diwaker Gupta + */ +@Test(groups = "unit") +public class StripExpectHeaderTest { + public void testExpectHeaderIsStripped() { + HttpRequest request = HttpRequest.builder().method("POST").addHeader(HttpHeaders.EXPECT, "100-Continue") + .endpoint("http://localhost").build(); + request = new StripExpectHeader().filter(request); + assertFalse(request.getHeaders().containsKey(HttpHeaders.EXPECT)); + } +} diff --git a/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java b/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java index 1a42f5729f..4e076666bb 100644 --- a/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java +++ b/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java @@ -24,6 +24,7 @@ import static org.jclouds.io.Payloads.newStringPayload; import static org.jclouds.reflect.Reflection2.method; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; import java.io.Closeable; @@ -42,9 +43,9 @@ import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Map; +import java.util.Properties; import java.util.Set; import java.util.concurrent.ExecutionException; - import javax.inject.Named; import javax.inject.Qualifier; import javax.inject.Singleton; @@ -62,6 +63,7 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import org.eclipse.jetty.http.HttpHeaders; +import org.jclouds.Constants; import org.jclouds.ContextBuilder; import org.jclouds.date.DateService; import org.jclouds.date.internal.SimpleDateFormatDateService; @@ -72,6 +74,7 @@ import org.jclouds.http.HttpRequest; import org.jclouds.http.HttpRequestFilter; import org.jclouds.http.HttpResponse; import org.jclouds.http.IOExceptionRetryHandler; +import org.jclouds.http.filters.StripExpectHeader; import org.jclouds.http.functions.ParseFirstJsonValueNamed; import org.jclouds.http.functions.ParseJson; import org.jclouds.http.functions.ParseSax; @@ -851,8 +854,8 @@ public class RestAnnotationProcessorTest extends BaseRestApiTest { Lists. newArrayList((String) null))); Assert.fail("call should have failed with illegal null parameter, not permitted " + request + " to be created"); } catch (NullPointerException e) { - Assert.assertTrue(e.toString().indexOf("postNonnull parameter 1") >= 0, - "Error message should have referred to 'parameter 1': " + e); + assertTrue(e.toString().indexOf("postNonnull parameter 1") >= 0, + "Error message should have referred to 'parameter 1': " + e); } } @@ -1367,6 +1370,9 @@ public class RestAnnotationProcessorTest extends BaseRestApiTest { @OverrideRequestFilters @RequestFilters(TestRequestFilter2.class) public void getOverride(HttpRequest request); + + @POST + public void post(); } @Test @@ -1396,6 +1402,37 @@ public class RestAnnotationProcessorTest extends BaseRestApiTest { assertEquals(request.getFilters().get(0).getClass(), TestRequestFilter2.class); } + @Test + public void testRequestFilterStripExpect() { + // First, verify that by default, the StripExpectHeader filter is not applied + Invokable method = method(TestRequestFilter.class, "post"); + Invocation invocation = Invocation.create(method, + ImmutableList.of(HttpRequest.builder().method("POST").endpoint("http://localhost") + .addHeader(HttpHeaders.EXPECT, "100-Continue").build())); + GeneratedHttpRequest request = processor.apply(invocation); + assertEquals(request.getFilters().size(), 1); + assertEquals(request.getFilters().get(0).getClass(), TestRequestFilter1.class); + + // Now let's create a new injector with the property set. Use that to create the annotation processor. + Properties overrides = new Properties(); + overrides.setProperty(Constants.PROPERTY_STRIP_EXPECT_HEADER, "true"); + Injector injector = ContextBuilder.newBuilder( + AnonymousProviderMetadata.forClientMappedToAsyncClientOnEndpoint(Callee.class, AsyncCallee.class, + "http://localhost:9999")) + .modules(ImmutableSet. of(new MockModule(), new NullLoggingModule(), new AbstractModule() { + protected void configure() { + bind(new TypeLiteral>() { + }).annotatedWith(Localhost2.class).toInstance( + Suppliers.ofInstance(URI.create("http://localhost:1111"))); + }})) + .overrides(overrides).buildInjector(); + RestAnnotationProcessor newProcessor = injector.getInstance(RestAnnotationProcessor.class); + // Verify that this time the filter is indeed applied as expected. + request = newProcessor.apply(invocation); + assertEquals(request.getFilters().size(), 2); + assertEquals(request.getFilters().get(1).getClass(), StripExpectHeader.class); + } + public class TestEncoding { @GET @Path("/{path1}/{path2}")