fix issue #1289 bug in zero length put

This commit is contained in:
adriancole 2013-02-01 15:09:31 -08:00
parent d3bf1288df
commit 9723c62f0f
5 changed files with 125 additions and 136 deletions

View File

@ -247,8 +247,10 @@ public class JavaUrlHttpCommandExecutorService extends BaseHttpCommandExecutorSe
} else { } else {
connection.setRequestProperty(CONTENT_LENGTH, "0"); connection.setRequestProperty(CONTENT_LENGTH, "0");
// for some reason POST/PUT undoes the content length header above. // for some reason POST/PUT undoes the content length header above.
if (ImmutableSet.of("POST", "PUT").contains(connection.getRequestMethod())) if (ImmutableSet.of("POST", "PUT").contains(connection.getRequestMethod())) {
connection.setFixedLengthStreamingMode(0); connection.setFixedLengthStreamingMode(0);
connection.setDoOutput(true);
}
} }
return connection; return connection;
} }

View File

@ -278,4 +278,9 @@ public abstract class BaseHttpCommandExecutorServiceIntegrationTest extends Base
TimeoutException { TimeoutException {
assertEquals(client.downloadAndParse(""), "whoppers"); assertEquals(client.downloadAndParse(""), "whoppers");
} }
@Test(invocationCount = 5, timeOut = 5000)
public void testZeroLengthPut() {
client.putNothing("");
}
} }

View File

@ -18,8 +18,8 @@
*/ */
package org.jclouds.http; package org.jclouds.http;
import static com.google.common.base.Throwables.propagate; import static com.google.common.base.Throwables.getStackTraceAsString;
import static com.google.common.collect.Maps.newHashMap; import static com.google.common.hash.Hashing.md5;
import static com.google.common.io.ByteStreams.copy; import static com.google.common.io.ByteStreams.copy;
import static com.google.common.io.ByteStreams.join; import static com.google.common.io.ByteStreams.join;
import static com.google.common.io.ByteStreams.newInputStreamSupplier; import static com.google.common.io.ByteStreams.newInputStreamSupplier;
@ -40,8 +40,8 @@ import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -50,7 +50,6 @@ import java.util.zip.GZIPInputStream;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.HttpHeaders;
import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Handler;
@ -59,19 +58,19 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.ssl.SslSelectChannelConnector; import org.eclipse.jetty.server.ssl.SslSelectChannelConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.jclouds.Constants;
import org.jclouds.ContextBuilder; import org.jclouds.ContextBuilder;
import org.jclouds.crypto.CryptoStreams; import org.jclouds.crypto.CryptoStreams;
import org.jclouds.io.InputSuppliers; import org.jclouds.io.InputSuppliers;
import org.jclouds.providers.AnonymousProviderMetadata; import org.jclouds.providers.AnonymousProviderMetadata;
import org.jclouds.rest.RestContext; import org.jclouds.rest.RestContext;
import org.jclouds.util.Strings2; import org.testng.annotations.AfterClass;
import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Optional; import org.testng.annotations.Optional;
import org.testng.annotations.Parameters; import org.testng.annotations.Parameters;
import com.google.common.base.Throwables; import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
@ -94,43 +93,42 @@ public abstract class BaseJettyTest {
protected String md5; protected String md5;
static final Pattern actionPattern = Pattern.compile("/objects/(.*)/action/([a-z]*);?(.*)"); static final Pattern actionPattern = Pattern.compile("/objects/(.*)/action/([a-z]*);?(.*)");
@BeforeTest @BeforeClass
@Parameters( { "test-jetty-port" }) @Parameters({ "test-jetty-port" })
public void setUpJetty(@Optional("8123") final int testPort) throws Exception { public void setUpJetty(@Optional("8123") final int testPort) throws Exception {
this.testPort = testPort; this.testPort = testPort;
final InputSupplier<InputStream> oneHundredOneConstitutions = getTestDataSupplier(); final InputSupplier<InputStream> oneHundredOneConstitutions = getTestDataSupplier();
md5 = CryptoStreams.md5Base64(oneHundredOneConstitutions); md5 = CryptoStreams.md5Base64(oneHundredOneConstitutions);
Handler server1Handler = new AbstractHandler() { Handler server1Handler = new AbstractHandler() {
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException { throws IOException, ServletException {
InputStream body = request.getInputStream();
try {
if (failIfNoContentLength(request, response)) { if (failIfNoContentLength(request, response)) {
return; return;
} else if (target.indexOf("sleep") > 0) { } else if (target.indexOf("sleep") > 0) {
try { sleepUninterruptibly(100, TimeUnit.MILLISECONDS);
Thread.sleep(100);
} catch (InterruptedException e) {
propagate(e);
}
response.setContentType("text/xml"); response.setContentType("text/xml");
response.setStatus(HttpServletResponse.SC_OK); response.setStatus(SC_OK);
} else if (target.indexOf("redirect") > 0) { } else if (target.indexOf("redirect") > 0) {
// in OpenJDK 7.0, expect continue handling is enforced, so we
// have to consume the stream.
// http://hg.openjdk.java.net/jdk7/tl/jdk/rev/045aeb76b0ff
// getInputStream address the expect-continue, per jetty docs
// http://wiki.eclipse.org/Jetty/Feature/1xx_Responses#100_Continue
toStringAndClose(request.getInputStream());
response.sendRedirect("https://localhost:" + (testPort + 1) + "/"); response.sendRedirect("https://localhost:" + (testPort + 1) + "/");
} else if (target.indexOf("101constitutions") > 0) { } else if (target.indexOf("101constitutions") > 0) {
response.setContentType("text/plain"); response.setContentType("text/plain");
response.setHeader("Content-MD5", md5); response.setHeader("Content-MD5", md5);
response.setStatus(HttpServletResponse.SC_OK); response.setStatus(SC_OK);
copy(oneHundredOneConstitutions.getInput(), response.getOutputStream()); copy(oneHundredOneConstitutions, response.getOutputStream());
} else if (request.getMethod().equals("PUT")) { } else if (request.getMethod().equals("PUT")) {
if (request.getContentLength() > 0) { if (request.getContentLength() > 0) {
response.setStatus(HttpServletResponse.SC_OK); response.setStatus(SC_OK);
response.getWriter().println(Strings2.toStringAndClose(body) + "PUT"); response.getWriter().println(toStringAndClose(request.getInputStream()) + "PUT");
} else { } else {
response.sendError(500, "no content"); response.setStatus(SC_OK);
} }
} else if (request.getMethod().equals("POST")) { } else if (request.getMethod().equals("POST")) {
// don't redirect large objects // don't redirect large objects
@ -147,27 +145,20 @@ public abstract class BaseJettyTest {
response.sendError(404, "no content"); response.sendError(404, "no content");
} else if (request.getHeader("test") != null) { } else if (request.getHeader("test") != null) {
response.setContentType("text/plain"); response.setContentType("text/plain");
response.setStatus(HttpServletResponse.SC_OK); response.setStatus(SC_OK);
response.getWriter().println("test"); response.getWriter().println("test");
} else if (request.getMethod().equals("HEAD")) { } else if (request.getMethod().equals("HEAD")) {
/* // by HTML specification, HEAD response MUST NOT include a body
* NOTE: by HTML specification, HEAD response MUST NOT include a body
*/
response.setContentType("text/xml"); response.setContentType("text/xml");
response.setStatus(HttpServletResponse.SC_OK); response.setStatus(SC_OK);
} else { } else {
if (failEveryTenRequests(request, response)) if (failEveryTenRequests(request, response))
return; return;
response.setContentType("text/xml"); response.setContentType("text/xml");
response.setStatus(HttpServletResponse.SC_OK); response.setStatus(SC_OK);
response.getWriter().println(XML); response.getWriter().println(XML);
} }
((Request) request).setHandled(true); Request.class.cast(request).setHandled(true);
} catch (IOException e) {
if (body != null)
closeQuietly(body);
response.sendError(500, Throwables.getStackTraceAsString(e));
}
} }
}; };
@ -196,26 +187,25 @@ public abstract class BaseJettyTest {
realMd5FromRequest = CryptoStreams.md5Base64(InputSuppliers.of(body)); realMd5FromRequest = CryptoStreams.md5Base64(InputSuppliers.of(body));
boolean matched = expectedMd5.equals(realMd5FromRequest); boolean matched = expectedMd5.equals(realMd5FromRequest);
if (matched) { if (matched) {
response.setStatus(HttpServletResponse.SC_OK); response.setStatus(SC_OK);
response.addHeader("x-Content-MD5", realMd5FromRequest); response.addHeader("x-Content-MD5", realMd5FromRequest);
} else { } else {
response.sendError(500, "didn't match"); response.sendError(500, "didn't match");
} }
} else { } else {
String responseString = (request.getContentLength() < 10240) ? Strings2.toStringAndClose(body) + "POST" String responseString = (request.getContentLength() < 10240) ? toStringAndClose(body) + "POST" : "POST";
: "POST";
body = null; body = null;
for (String header : new String[] { CONTENT_DISPOSITION, CONTENT_LANGUAGE, CONTENT_ENCODING }) for (String header : new String[] { CONTENT_DISPOSITION, CONTENT_LANGUAGE, CONTENT_ENCODING })
if (request.getHeader(header) != null) { if (request.getHeader(header) != null) {
response.addHeader("x-" + header, request.getHeader(header)); response.addHeader("x-" + header, request.getHeader(header));
} }
response.setStatus(HttpServletResponse.SC_OK); response.setStatus(SC_OK);
response.getWriter().println(responseString); response.getWriter().println(responseString);
} }
Request.class.cast(request).setHandled(true);
} catch (IOException e) { } catch (IOException e) {
if (body != null)
closeQuietly(body); closeQuietly(body);
response.sendError(500, Throwables.getStackTraceAsString(e)); response.sendError(500, getStackTraceAsString(e));
} }
} }
@ -223,13 +213,10 @@ public abstract class BaseJettyTest {
Handler server2Handler = new AbstractHandler() { Handler server2Handler = new AbstractHandler() {
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException { throws IOException, ServletException {
InputStream body = request.getInputStream();
try {
if (request.getMethod().equals("PUT")) { if (request.getMethod().equals("PUT")) {
String text = Strings2.toStringAndClose(body);
body = null;
if (request.getContentLength() > 0) { if (request.getContentLength() > 0) {
response.setStatus(HttpServletResponse.SC_OK); response.setStatus(SC_OK);
String text = toStringAndClose(request.getInputStream());
response.getWriter().println(text + "PUTREDIRECT"); response.getWriter().println(text + "PUTREDIRECT");
} }
} else if (request.getMethod().equals("POST")) { } else if (request.getMethod().equals("POST")) {
@ -239,22 +226,15 @@ public abstract class BaseJettyTest {
handleAction(request, response); handleAction(request, response);
} }
} else if (request.getMethod().equals("HEAD")) { } else if (request.getMethod().equals("HEAD")) {
/* // by HTML specification, HEAD response MUST NOT include a body
* NOTE: by HTML specification, HEAD response MUST NOT include a body
*/
response.setContentType("text/xml"); response.setContentType("text/xml");
response.setStatus(HttpServletResponse.SC_OK); response.setStatus(SC_OK);
} else { } else {
response.setContentType("text/xml"); response.setContentType("text/xml");
response.setStatus(HttpServletResponse.SC_OK); response.setStatus(SC_OK);
response.getWriter().println(XML2); response.getWriter().println(XML2);
} }
((Request) request).setHandled(true); Request.class.cast(request).setHandled(true);
} catch (IOException e) {
if (body != null)
closeQuietly(body);
response.sendError(500, Throwables.getStackTraceAsString(e));
}
} }
}; };
@ -270,7 +250,7 @@ public abstract class BaseJettyTest {
ssl.setTrustStore("src/test/resources/test.jks"); ssl.setTrustStore("src/test/resources/test.jks");
ssl.setTrustStorePassword("jclouds"); ssl.setTrustStorePassword("jclouds");
server2.setConnectors(new Connector[]{ ssl_connector }); server2.setConnectors(new Connector[] { ssl_connector });
server2.start(); server2.start();
} }
@ -289,18 +269,18 @@ public abstract class BaseJettyTest {
} }
public static ContextBuilder newBuilder(int testPort, Properties properties, Module... connectionModules) { public static ContextBuilder newBuilder(int testPort, Properties properties, Module... connectionModules) {
properties.setProperty(Constants.PROPERTY_TRUST_ALL_CERTS, "true"); properties.setProperty(PROPERTY_TRUST_ALL_CERTS, "true");
properties.setProperty(Constants.PROPERTY_RELAX_HOSTNAME, "true"); properties.setProperty(PROPERTY_RELAX_HOSTNAME, "true");
return ContextBuilder.newBuilder( return ContextBuilder
AnonymousProviderMetadata.forClientMappedToAsyncClientOnEndpoint(IntegrationTestClient.class, IntegrationTestAsyncClient.class, .newBuilder(
"http://localhost:" + testPort)) AnonymousProviderMetadata.forClientMappedToAsyncClientOnEndpoint(IntegrationTestClient.class,
.modules(ImmutableSet.<Module> copyOf(connectionModules)) IntegrationTestAsyncClient.class, "http://localhost:" + testPort))
.overrides(properties); .modules(ImmutableSet.<Module> copyOf(connectionModules)).overrides(properties);
} }
@AfterTest @AfterClass
public void tearDownJetty() throws Exception { public void tearDownJetty() throws Exception {
context.close(); closeQuietly(context);
if (server2 != null) if (server2 != null)
server2.stop(); server2.stop();
server.stop(); server.stop();
@ -321,7 +301,7 @@ public abstract class BaseJettyTest {
protected boolean failEveryTenRequests(HttpServletRequest request, HttpServletResponse response) throws IOException { protected boolean failEveryTenRequests(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (cycle.incrementAndGet() % 10 == 0) { if (cycle.incrementAndGet() % 10 == 0) {
response.sendError(500, "unlucky 10"); response.sendError(500, "unlucky 10");
((Request) request).setHandled(true); Request.class.cast(request).setHandled(true);
return true; return true;
} }
return false; return false;
@ -331,7 +311,7 @@ public abstract class BaseJettyTest {
throws IOException { throws IOException {
if (cycle.incrementAndGet() % 20 == 0) { if (cycle.incrementAndGet() % 20 == 0) {
response.sendRedirect("http://localhost:" + (testPort + 1) + "/"); response.sendRedirect("http://localhost:" + (testPort + 1) + "/");
((Request) request).setHandled(true); Request.class.cast(request).setHandled(true);
return true; return true;
} }
return false; return false;
@ -351,7 +331,7 @@ public abstract class BaseJettyTest {
response.getWriter().println("no content length!"); response.getWriter().println("no content length!");
response.getWriter().println(realHeaders.toString()); response.getWriter().println(realHeaders.toString());
response.sendError(500, "no content length!"); response.sendError(500, "no content length!");
((Request) request).setHandled(true); Request.class.cast(request).setHandled(true);
return true; return true;
} }
return false; return false;
@ -363,20 +343,15 @@ public abstract class BaseJettyTest {
if (matchFound) { if (matchFound) {
String objectId = matcher.group(1); String objectId = matcher.group(1);
String action = matcher.group(2); String action = matcher.group(2);
Map<String, String> options = newHashMap(); Builder<String, String> options = ImmutableMap.<String, String> builder();
if (matcher.groupCount() == 3) { if (matcher.groupCount() == 3) {
String optionsGroup = matcher.group(3); options.putAll(Splitter.on(';').withKeyValueSeparator("=").split(matcher.group(3)));
for (String entry : optionsGroup.split(";")) {
if (entry.indexOf('=') >= 0) {
String[] keyValue = entry.split("=");
options.put(keyValue[0], keyValue[1]);
} }
} response.setStatus(SC_OK);
} response.getWriter().println(objectId + "->" + action + ":" + options.build());
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().println(objectId + "->" + action + ":" + options);
} else { } else {
response.sendError(500, "no content"); response.sendError(500, "no content");
} }
} }
} }

View File

@ -56,6 +56,7 @@ import com.google.inject.Provides;
/** /**
* Sample test for the behaviour of our Integration Test jetty server. * Sample test for the behaviour of our Integration Test jetty server.
* *
* @see IntegrationTestClient
* @author Adrian Cole * @author Adrian Cole
*/ */
public interface IntegrationTestAsyncClient { public interface IntegrationTestAsyncClient {
@ -192,6 +193,10 @@ public interface IntegrationTestAsyncClient {
} }
@PUT
@Path("/objects/{id}")
ListenableFuture<Void> putNothing(@PathParam("id") String id);
@Provides @Provides
StringBuilder newStringBuilder(); StringBuilder newStringBuilder();

View File

@ -64,6 +64,8 @@ public interface IntegrationTestClient {
String downloadAndParse(String id); String downloadAndParse(String id);
void putNothing(String id);
@Provides @Provides
StringBuilder newStringBuilder(); StringBuilder newStringBuilder();
} }