You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by jp...@apache.org on 2014/03/02 18:50:50 UTC

git commit: TS-2553: improve metalink handling of event completion

Repository: trafficserver
Updated Branches:
  refs/heads/master 203fc9971 -> be1f21410


TS-2553: improve metalink handling of event completion

  - if we are not at the end yet and can't read any more content then don't compute the digest
  - zero the downstream nbytes as a shortcut to get it to send a TS_EVENT_VCONN_WRITE_COMPLETE event
  - avoid failed assert "sdk_sanity_check_iocore_structure(connp) == TS_SUCCESS" in TSVConnWrite() if the response is
  304 Not Modified
  - add some functional tests

This closes #51.


Project: http://git-wip-us.apache.org/repos/asf/trafficserver/repo
Commit: http://git-wip-us.apache.org/repos/asf/trafficserver/commit/be1f2141
Tree: http://git-wip-us.apache.org/repos/asf/trafficserver/tree/be1f2141
Diff: http://git-wip-us.apache.org/repos/asf/trafficserver/diff/be1f2141

Branch: refs/heads/master
Commit: be1f214104b5f831fcbaf437782455c1c13eb096
Parents: 203fc99
Author: Jack Bates <ja...@nottheoilrig.com>
Authored: Thu Feb 20 13:00:30 2014 -0800
Committer: James Peach <jp...@apache.org>
Committed: Sun Mar 2 09:39:35 2014 -0800

----------------------------------------------------------------------
 plugins/experimental/metalink/metalink.cc       |  80 +++++++-------
 .../experimental/metalink/test/chunkedEncoding  |  97 ++++++++++++++++
 .../metalink/test/chunkedEncodingDisconnect     |  97 ++++++++++++++++
 .../experimental/metalink/test/clientDisconnect |  94 ++++++++++++++++
 .../experimental/metalink/test/contentLength    |  99 +++++++++++++++++
 .../metalink/test/contentLengthDisconnect       |  92 ++++++++++++++++
 .../test/finalChunkedEncodingDisconnect         | 110 +++++++++++++++++++
 plugins/experimental/metalink/test/http09       |  84 ++++++++++++++
 plugins/experimental/metalink/test/longer       |  92 ++++++++++++++++
 plugins/experimental/metalink/test/notModified  |  77 +++++++++++++
 .../test/shortChunkedEncodingDisconnect         |  96 ++++++++++++++++
 .../metalink/test/shortClientDisconnect         |  90 +++++++++++++++
 .../metalink/test/shortContentLengthDisconnect  |  93 ++++++++++++++++
 plugins/experimental/metalink/test/zero         |  90 +++++++++++++++
 14 files changed, 1253 insertions(+), 38 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/trafficserver/blob/be1f2141/plugins/experimental/metalink/metalink.cc
----------------------------------------------------------------------
diff --git a/plugins/experimental/metalink/metalink.cc b/plugins/experimental/metalink/metalink.cc
index a098e1b..ea253dd 100644
--- a/plugins/experimental/metalink/metalink.cc
+++ b/plugins/experimental/metalink/metalink.cc
@@ -234,7 +234,17 @@ vconn_write_ready(TSCont contp, void * /* edata ATS_UNUSED */)
   /* Initialize data here because can't call TSVConnWrite() before
    * TS_HTTP_RESPONSE_TRANSFORM_HOOK */
   if (!data->output_bufp) {
+
+    /* Avoid failed assert "sdk_sanity_check_iocore_structure(connp) ==
+     * TS_SUCCESS" in TSVConnWrite() if the response is 304 Not Modified */
     TSVConn output_connp = TSTransformOutputVConnGet(contp);
+    if (!output_connp) {
+      TSContDestroy(contp);
+
+      TSfree(data);
+
+      return 0;
+    }
 
     data->output_bufp = TSIOBufferCreate();
     TSIOBufferReader readerp = TSIOBufferReaderAlloc(data->output_bufp);
@@ -259,12 +269,11 @@ vconn_write_ready(TSCont contp, void * /* edata ATS_UNUSED */)
    * nbytes is INT64_MAX.
    *
    * In that case to get it to send a TS_EVENT_VCONN_WRITE_COMPLETE event,
-   * update the downstream nbytes and reenable it. */
+   * update the downstream nbytes and reenable it.  Zero the downstream nbytes
+   * is a shortcut. */
   int ntodo = TSVIONTodoGet(input_viop);
   if (!ntodo) {
-
-    int ndone = TSVIONDoneGet(input_viop);
-    TSVIONBytesSet(data->output_viop, ndone);
+    TSVIONBytesSet(data->output_viop, 0);
 
     TSVIOReenable(data->output_viop);
 
@@ -272,25 +281,22 @@ vconn_write_ready(TSCont contp, void * /* edata ATS_UNUSED */)
   }
 
   /* Avoid failed assert "sdk_sanity_check_iocore_structure(readerp) ==
-   * TS_SUCCESS" in TSIOBufferReaderAvail() if the status code is 302?  or the
-   * message body is empty? */
+   * TS_SUCCESS" in TSIOBufferReaderAvail() if the client or server disconnects
+   * or the content length is zero.
+   *
+   * Don't update the downstream nbytes and reenable it because if not at the
+   * end yet and can't read any more content then can't compute the digest.
+   *
+   * (There hasn't been a TS_EVENT_VCONN_WRITE_COMPLETE event from downstream
+   * yet so if the response has a "Content-Length: ..." header, it is greater
+   * than the content so far.  ntodo is still greater than zero so if the
+   * response is "Transfer-Encoding: chunked", not at the end yet.) */
   TSIOBufferReader readerp = TSVIOReaderGet(input_viop);
   if (!readerp) {
+    TSContDestroy(contp);
 
-    /* Avoid segfault in TSVIOReenable() if the client disconnected */
-    if (TSVConnClosedGet(contp)) {
-      TSContDestroy(contp);
-
-      TSIOBufferDestroy(data->output_bufp);
-      TSfree(data);
-
-    } else {
-
-      int ndone = TSVIONDoneGet(input_viop);
-      TSVIONBytesSet(data->output_viop, ndone);
-
-      TSVIOReenable(data->output_viop);
-    }
+    TSIOBufferDestroy(data->output_bufp);
+    TSfree(data);
 
     return 0;
   }
@@ -566,6 +572,12 @@ digest_handler(TSCont contp, TSEvent event, void *edata)
 static int
 location_handler(TSCont contp, TSEvent event, void * /* edata ATS_UNUSED */)
 {
+  const char *value;
+  int length;
+
+  /* ATS_BASE64_DECODE_DSTLEN() */
+  char digest[33];
+
   SendData *data = (SendData *) TSContDataGet(contp);
   TSContDestroy(contp);
 
@@ -576,28 +588,20 @@ location_handler(TSCont contp, TSEvent event, void * /* edata ATS_UNUSED */)
 
   /* No: Check if the "Digest: SHA-256=..." digest already exists in the cache */
   case TS_EVENT_CACHE_OPEN_READ_FAILED:
-    {
-      const char *value;
-      int length;
-
-      /* ATS_BASE64_DECODE_DSTLEN() */
-      char digest[33];
 
-      value = TSMimeHdrFieldValueStringGet(data->resp_bufp, data->hdr_loc, data->digest_loc, data->idx, &length);
-      if (TSBase64Decode(value + 8, length - 8, (unsigned char *) digest, sizeof(digest), NULL) != TS_SUCCESS
-          || TSCacheKeyDigestSet(data->key, digest, 32 /* SHA-256 */ ) != TS_SUCCESS) {
-        break;
-      }
+    value = TSMimeHdrFieldValueStringGet(data->resp_bufp, data->hdr_loc, data->digest_loc, data->idx, &length);
+    if (TSBase64Decode(value + 8, length - 8, (unsigned char *) digest, sizeof(digest), NULL) != TS_SUCCESS
+        || TSCacheKeyDigestSet(data->key, digest, 32 /* SHA-256 */ ) != TS_SUCCESS) {
+      break;
+    }
 
-      contp = TSContCreate(digest_handler, NULL);
-      TSContDataSet(contp, data);
+    contp = TSContCreate(digest_handler, NULL);
+    TSContDataSet(contp, data);
 
-      TSCacheRead(contp, data->key);
-      TSHandleMLocRelease(data->resp_bufp, data->hdr_loc, data->digest_loc);
+    TSCacheRead(contp, data->key);
+    TSHandleMLocRelease(data->resp_bufp, data->hdr_loc, data->digest_loc);
 
-      return 0;
-    }
-    break;
+    return 0;
 
   default:
     TSAssert(!"Unexpected event");

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/be1f2141/plugins/experimental/metalink/test/chunkedEncoding
----------------------------------------------------------------------
diff --git a/plugins/experimental/metalink/test/chunkedEncoding b/plugins/experimental/metalink/test/chunkedEncoding
new file mode 100755
index 0000000..ece4768
--- /dev/null
+++ b/plugins/experimental/metalink/test/chunkedEncoding
@@ -0,0 +1,97 @@
+#!/usr/bin/env python
+
+#  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.
+
+print '''1..1 chunkedEncoding
+# The proxy forwards the final chunk at the end of a chunked response'''
+
+from twisted.internet import error, protocol, reactor, tcp
+from twisted.web import http
+
+def callback():
+  print 'not ok 1 - No final chunk yet'
+
+  reactor.stop()
+
+reactor.callLater(2, callback)
+
+class factory(http.HTTPFactory):
+  class protocol(http.HTTPChannel):
+    class requestFactory(http.Request):
+      def requestReceived(ctx, method, target, version):
+
+        ctx.client = None
+        ctx.clientproto = version
+
+        ctx.write('chunkedEncoding')
+
+        # If the proxy reads the final chunk before it sends the response
+        # headers, it may send a Content-Length header vs. a chunked response
+        reactor.callLater(1, ctx.finish)
+
+server = tcp.Port(0, factory())
+server.startListening()
+
+print '# Listening on {0}:{1}'.format(*server.socket.getsockname())
+
+class factory(protocol.ClientFactory):
+  def clientConnectionFailed(ctx, connector, reason):
+
+    print 'Bail out!'
+    reason.printTraceback()
+
+    reactor.stop()
+
+  class protocol(http.HTTPClient):
+    def connectionLost(ctx, reason):
+      try:
+        reactor.stop()
+
+      except error.ReactorNotRunning:
+        pass
+
+      else:
+        print 'not ok 1 - Did the proxy crash?  (The client connection closed.)'
+
+    connectionMade = lambda ctx: ctx.transport.write('GET {0}:{1} HTTP/1.1\r\n\r\n'.format(*server.socket.getsockname()))
+
+    def handleHeader(ctx, k, v):
+      if k.lower() == 'content-length':
+        print 'not ok 1 - Got a Content-Length header vs. a chunked response'
+
+        # No hope of a final chunk now
+        reactor.stop()
+
+    # Avoid calling undefined handleResponse() at the end of the content (if
+    # the proxy sent a Content-Length header vs. a chunked response).
+    # (Override connectionLost() when the proxy crashes or stop the reactor.)
+    #
+    # The data that was already received will be processed (the end of the
+    # headers), then shutdown events will fire (connections will be closed),
+    # and then finally the reactor will grind to a halt.
+    def handleResponseEnd(ctx):
+      pass
+
+    def handleResponsePart(ctx, data):
+      if data.endswith('0\r\n\r\n'):
+        print 'ok 1 - Got the final chunk'
+
+        reactor.stop()
+
+tcp.Connector('localhost', 8080, factory(), 30, None, reactor).connect()
+
+reactor.run()

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/be1f2141/plugins/experimental/metalink/test/chunkedEncodingDisconnect
----------------------------------------------------------------------
diff --git a/plugins/experimental/metalink/test/chunkedEncodingDisconnect b/plugins/experimental/metalink/test/chunkedEncodingDisconnect
new file mode 100755
index 0000000..3c0210b
--- /dev/null
+++ b/plugins/experimental/metalink/test/chunkedEncodingDisconnect
@@ -0,0 +1,97 @@
+#!/usr/bin/env python
+
+#  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.
+
+print '''1..1 chunkedEncodingDisconnect
+# The proxy closes the client connection and doesn't send a final chunk if the
+# server disconnects without sending one'''
+
+from twisted.internet import error, protocol, reactor, tcp
+from twisted.web import http
+
+def callback():
+  print 'not ok 1 - The client was left hanging'
+
+  reactor.stop()
+
+reactor.callLater(2, callback)
+
+class factory(http.HTTPFactory):
+  class protocol(http.HTTPChannel):
+    class requestFactory(http.Request):
+      def requestReceived(ctx, method, target, version):
+
+        ctx.client = None
+        ctx.clientproto = version
+
+        ctx.write('chunkedEncodingDisconnect')
+
+        # If the server disconnects before the proxy sends the response
+        # headers, the proxy may send a Content-Length header vs. a chunked
+        # response
+        reactor.callLater(1, ctx.transport.loseConnection)
+
+server = tcp.Port(0, factory())
+server.startListening()
+
+print '# Listening on {0}:{1}'.format(*server.socket.getsockname())
+
+class factory(protocol.ClientFactory):
+  def clientConnectionFailed(ctx, connector, reason):
+
+    print 'Bail out!'
+    reason.printTraceback()
+
+    reactor.stop()
+
+  class protocol(http.HTTPClient):
+    def connectionLost(ctx, reason):
+      try:
+        reactor.stop()
+
+      except error.ReactorNotRunning:
+        pass
+
+      else:
+        print 'ok 1 - The client connection closed'
+
+    connectionMade = lambda ctx: ctx.transport.write('GET {0}:{1} HTTP/1.1\r\n\r\n'.format(*server.socket.getsockname()))
+
+    def handleHeader(ctx, k, v):
+      if k.lower() == 'content-length':
+        print 'not ok 1 - Got a Content-Length header vs. a chunked response'
+
+        # Who cares what happens now?
+        reactor.stop()
+
+    # Avoid calling undefined handleResponse() at the end of the content (if
+    # the proxy sent a Content-Length header vs. a chunked response).
+    # (Override connectionLost() when the proxy closes the client connection or
+    # stop the reactor.)
+    def handleResponseEnd(ctx):
+      pass
+
+    def handleResponsePart(ctx, data):
+      if data.endswith('0\r\n\r\n'):
+        print 'not ok 1 - Got a final chunk'
+
+        # Who cares what happens now?
+        reactor.stop()
+
+tcp.Connector('localhost', 8080, factory(), 30, None, reactor).connect()
+
+reactor.run()

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/be1f2141/plugins/experimental/metalink/test/clientDisconnect
----------------------------------------------------------------------
diff --git a/plugins/experimental/metalink/test/clientDisconnect b/plugins/experimental/metalink/test/clientDisconnect
new file mode 100755
index 0000000..b8ee3da
--- /dev/null
+++ b/plugins/experimental/metalink/test/clientDisconnect
@@ -0,0 +1,94 @@
+#!/usr/bin/env python
+
+#  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.
+
+print '''1..1 clientDissconnect
+# The proxy doesn't crash if the client disconnects prematurely'''
+
+from twisted.internet import error, protocol, reactor, tcp
+from twisted.web import http
+
+def callback():
+  print 'not ok 1 - Why didn\'t the test finish yet?'
+
+  reactor.stop()
+
+reactor.callLater(3, callback)
+
+class factory(http.HTTPFactory):
+  class protocol(http.HTTPChannel):
+    class requestFactory(http.Request):
+      def requestReceived(ctx, method, target, version):
+
+        ctx.client = None
+        ctx.clientproto = version
+
+        ctx.write('clientDisconnect')
+
+        # The proxy crashes only after the response is complete
+        def callback():
+          try:
+            ctx.finish()
+
+          except RuntimeError:
+            pass
+
+          # Open another connection
+          class factory(protocol.ClientFactory):
+            def clientConnectionFailed(ctx, connector, reason):
+              print 'not ok 1 - Did the proxy crash?  (Can\'t open another connection to it.)'
+
+              reactor.stop()
+
+            class protocol(protocol.Protocol):
+              def connectionMade(ctx):
+                print 'ok 1 - The proxy didn\'t crash (opened another connection to it)'
+
+                reactor.stop()
+
+          reactor.callLater(1, tcp.Connector('localhost', 8080, factory(), 30, None, reactor).connect)
+
+        reactor.callLater(1, callback)
+
+server = tcp.Port(0, factory())
+server.startListening()
+
+print '# Listening on {0}:{1}'.format(*server.socket.getsockname())
+
+class factory(protocol.ClientFactory):
+  def clientConnectionFailed(ctx, connector, reason):
+
+    print 'Bail out!'
+    reason.printTraceback()
+
+    reactor.stop()
+
+  class protocol(http.HTTPClient):
+    def connectionMade(ctx):
+      ctx.transport.write('GET {0}:{1} HTTP/1.1\r\n\r\n'.format(*server.socket.getsockname()))
+
+      # Disconnect after the proxy sends the response headers
+      reactor.callLater(1, ctx.transport.loseConnection)
+
+    # Avoid calling undefined handleResponse() at the end of the content or
+    # when the connection closes
+    def handleResponseEnd(ctx):
+      pass
+
+tcp.Connector('localhost', 8080, factory(), 30, None, reactor).connect()
+
+reactor.run()

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/be1f2141/plugins/experimental/metalink/test/contentLength
----------------------------------------------------------------------
diff --git a/plugins/experimental/metalink/test/contentLength b/plugins/experimental/metalink/test/contentLength
new file mode 100755
index 0000000..c3005ac
--- /dev/null
+++ b/plugins/experimental/metalink/test/contentLength
@@ -0,0 +1,99 @@
+#!/usr/bin/env python
+
+#  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.
+
+print '''1..1 contentLength
+# The proxy forwards the Content-Length header to the client'''
+
+from twisted.internet import error, protocol, reactor, tcp
+from twisted.web import http
+
+def callback():
+  print 'not ok 1 - Why didn\'t the test finish yet?'
+
+  reactor.stop()
+
+reactor.callLater(1, callback)
+
+class factory(http.HTTPFactory):
+  class protocol(http.HTTPChannel):
+    class requestFactory(http.Request):
+      def requestReceived(ctx, method, target, version):
+
+        ctx.client = None
+        ctx.clientproto = version
+
+        ctx.setHeader('Content-Length', 13)
+        ctx.write('contentLength')
+
+server = tcp.Port(0, factory())
+server.startListening()
+
+print '# Listening on {0}:{1}'.format(*server.socket.getsockname())
+
+class factory(protocol.ClientFactory):
+  def clientConnectionFailed(ctx, connector, reason):
+
+    print 'Bail out!'
+    reason.printTraceback()
+
+    reactor.stop()
+
+  class protocol(http.HTTPClient):
+    def connectionLost(ctx, reason):
+      try:
+        reactor.stop()
+
+      except error.ReactorNotRunning:
+        pass
+
+      else:
+        print 'not ok 1 - Did the proxy crash?  (The client connection closed.)'
+
+    connectionMade = lambda ctx: ctx.transport.write('GET {0}:{1} HTTP/1.1\r\n\r\n'.format(*server.socket.getsockname()))
+
+    def handleEndHeaders(ctx):
+      try:
+        reactor.stop()
+
+      except error.ReactorNotRunning:
+        pass
+
+      else:
+        print 'not ok 1 - No Content-Length header'
+
+    def handleHeader(ctx, k, v):
+      if k.lower() == 'content-length':
+        if v != '13':
+          print 'not',
+
+        print 'ok 1 - Content-Length header'
+
+        reactor.stop()
+
+    # Avoid calling undefined handleResponse() at the end of the content.
+    # (Override connectionLost() when the proxy crashes or stop the reactor.)
+    #
+    # The data that was already received will be processed (the end of the
+    # headers), then shutdown events will fire (connections will be closed),
+    # and then finally the reactor will grind to a halt.
+    def handleResponseEnd(ctx):
+      pass
+
+tcp.Connector('localhost', 8080, factory(), 30, None, reactor).connect()
+
+reactor.run()

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/be1f2141/plugins/experimental/metalink/test/contentLengthDisconnect
----------------------------------------------------------------------
diff --git a/plugins/experimental/metalink/test/contentLengthDisconnect b/plugins/experimental/metalink/test/contentLengthDisconnect
new file mode 100755
index 0000000..2c98ae8
--- /dev/null
+++ b/plugins/experimental/metalink/test/contentLengthDisconnect
@@ -0,0 +1,92 @@
+#!/usr/bin/env python
+
+#  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.
+
+print '''1..2 contentLengthDisconnect
+# The proxy closes the client connection if the server disconnects prematurely'''
+
+from twisted.internet import error, protocol, reactor, tcp
+from twisted.web import http
+
+def callback():
+  print 'not ok 2 - The client was left hanging'
+
+  reactor.stop()
+
+reactor.callLater(2, callback)
+
+class factory(http.HTTPFactory):
+  class protocol(http.HTTPChannel):
+    class requestFactory(http.Request):
+      def requestReceived(ctx, method, target, version):
+
+        ctx.client = None
+        ctx.clientproto = version
+
+        ctx.setHeader('Content-Length', 24)
+        ctx.write('contentLengthDisconnect')
+
+        # If the server disconnects before the proxy sends the response
+        # headers, the proxy may send the wrong Content-Length header
+        reactor.callLater(1, ctx.transport.loseConnection)
+
+server = tcp.Port(0, factory())
+server.startListening()
+
+print '# Listening on {0}:{1}'.format(*server.socket.getsockname())
+
+class factory(protocol.ClientFactory):
+  def clientConnectionFailed(ctx, connector, reason):
+
+    print 'Bail out!'
+    reason.printTraceback()
+
+    reactor.stop()
+
+  class protocol(http.HTTPClient):
+    def connectionLost(ctx, reason):
+      try:
+        reactor.stop()
+
+      except error.ReactorNotRunning:
+        pass
+
+      else:
+        print 'ok 2 - The client connection closed'
+
+    connectionMade = lambda ctx: ctx.transport.write('GET {0}:{1} HTTP/1.1\r\n\r\n'.format(*server.socket.getsockname()))
+
+    def handleHeader(ctx, k, v):
+      if k.lower() == 'content-length':
+        if v != '24':
+          print 'not',
+
+          # Who cares what happens now?
+          reactor.stop()
+
+        print 'ok 1 - Content-Length header'
+
+    # Avoid calling undefined handleResponse() at the end of the content (if
+    # the proxy sent the wrong Content-Length header).  (Override
+    # connectionLost() when the proxy closes the client connection or stop the
+    # reactor.)
+    def handleResponseEnd(ctx):
+      pass
+
+tcp.Connector('localhost', 8080, factory(), 30, None, reactor).connect()
+
+reactor.run()

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/be1f2141/plugins/experimental/metalink/test/finalChunkedEncodingDisconnect
----------------------------------------------------------------------
diff --git a/plugins/experimental/metalink/test/finalChunkedEncodingDisconnect b/plugins/experimental/metalink/test/finalChunkedEncodingDisconnect
new file mode 100755
index 0000000..9fa4093
--- /dev/null
+++ b/plugins/experimental/metalink/test/finalChunkedEncodingDisconnect
@@ -0,0 +1,110 @@
+#!/usr/bin/env python
+
+#  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.
+
+print '''1..1 finalChunkEncodingDisconnect
+# The proxy forwards the final chunk even if the server disconnects immediately
+# afterward'''
+
+from twisted.internet import error, protocol, reactor, tcp
+from twisted.web import http
+
+def callback():
+  print 'not ok 1 - No final chunk yet'
+
+  reactor.stop()
+
+reactor.callLater(2, callback)
+
+class factory(http.HTTPFactory):
+  class protocol(http.HTTPChannel):
+    class requestFactory(http.Request):
+      def requestReceived(ctx, method, target, version):
+
+        ctx.client = None
+        ctx.clientproto = version
+
+        ctx.write('finalChunkedEncodingDisconnect')
+
+        # If the proxy reads the final chunk before it sends the response
+        # headers, it may send a Content-Length header vs. a chunked response
+        def callback():
+          try:
+            ctx.finish()
+
+          except RuntimeError:
+            print 'not ok 1 - Did the proxy crash?  (The server connection closed.)'
+
+            reactor.stop()
+
+          else:
+            ctx.transport.loseConnection()
+
+        reactor.callLater(1, callback)
+
+server = tcp.Port(0, factory())
+server.startListening()
+
+print '# Listening on {0}:{1}'.format(*server.socket.getsockname())
+
+class factory(protocol.ClientFactory):
+  def clientConnectionFailed(ctx, connector, reason):
+
+    print 'Bail out!'
+    reason.printTraceback()
+
+    reactor.stop()
+
+  class protocol(http.HTTPClient):
+    def connectionLost(ctx, reason):
+      try:
+        reactor.stop()
+
+      except error.ReactorNotRunning:
+        pass
+
+      else:
+        print 'not ok 1 - Did the proxy crash?  (The client connection closed.)'
+
+    connectionMade = lambda ctx: ctx.transport.write('GET {0}:{1} HTTP/1.1\r\n\r\n'.format(*server.socket.getsockname()))
+
+    def handleHeader(ctx, k, v):
+      if k.lower() == 'content-length':
+        print 'not ok 1 - Got a Content-Length header vs. a chunked response'
+
+        # No hope of a final chunk now
+        reactor.stop()
+
+    # Avoid calling undefined handleResponse() at the end of the content (if
+    # the proxy sent a Content-Length header vs. a chunked response).
+    # (Override connectionLost() when the proxy crashes or stop the reactor.)
+    #
+    # The data that was already received will be processed (the end of the
+    # headers), then shutdown events will fire (connections will be closed),
+    # and then finally the reactor will grind to a halt.
+    def handleResponseEnd(ctx):
+      pass
+
+    def handleResponsePart(ctx, data):
+      if data.endswith('0\r\n\r\n'):
+        print 'ok 1 - Got the final chunk'
+
+        reactor.stop()
+
+tcp.Connector('localhost', 8080, factory(), 30, None, reactor).connect()
+
+reactor.run()

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/be1f2141/plugins/experimental/metalink/test/http09
----------------------------------------------------------------------
diff --git a/plugins/experimental/metalink/test/http09 b/plugins/experimental/metalink/test/http09
new file mode 100755
index 0000000..9103c8d
--- /dev/null
+++ b/plugins/experimental/metalink/test/http09
@@ -0,0 +1,84 @@
+#!/usr/bin/env python
+
+#  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.
+
+print '''1..1 http09
+# The proxy doesn't crash on an HTTP/0.9 response'''
+
+# http://www.w3.org/Protocols/HTTP/AsImplemented
+#
+# The proxy crashes only after the response is complete.  It closes the client
+# connection whether it crashes or not because an HTTP/0.9 response is complete
+# only after the server closes its connection, and then the proxy normally does
+# the same thing to the client connection (although it upgrades the response to
+# HTTP/1.1).  So the only way to check that the proxy didn't crash is to open
+# another connection.
+
+from twisted.internet import error, protocol, reactor, tcp
+from twisted.web import http
+
+def callback():
+  print 'not ok 1 - Why didn\'t the test finish yet?'
+
+  reactor.stop()
+
+reactor.callLater(2, callback)
+
+class factory(protocol.Factory):
+  class protocol(protocol.Protocol):
+    def connectionMade(ctx):
+      ctx.transport.write('http09\r\n')
+
+      # The proxy crashes only after the response is complete
+      ctx.transport.loseConnection()
+
+server = tcp.Port(0, factory())
+server.startListening()
+
+print '# Listening on {0}:{1}'.format(*server.socket.getsockname())
+
+class factory(protocol.ClientFactory):
+  def clientConnectionFailed(ctx, connector, reason):
+
+    print 'Bail out!'
+    reason.printTraceback()
+
+    reactor.stop()
+
+  class protocol(http.HTTPClient):
+    def connectionLost(ctx, reason):
+
+      # Open another connection
+      class factory(protocol.ClientFactory):
+        def clientConnectionFailed(ctx, connector, reason):
+          print 'not ok 1 - Did the proxy crash?  (Can\'t open another connection to it.)'
+
+          reactor.stop()
+
+        class protocol(protocol.Protocol):
+          def connectionMade(ctx):
+            print 'ok 1 - The proxy didn\'t crash (opened another connection to it)'
+
+            reactor.stop()
+
+      reactor.callLater(1, tcp.Connector('localhost', 8080, factory(), 30, None, reactor).connect)
+
+    connectionMade = lambda ctx: ctx.transport.write('GET {0}:{1} HTTP/1.1\r\n\r\n'.format(*server.socket.getsockname()))
+
+tcp.Connector('localhost', 8080, factory(), 30, None, reactor).connect()
+
+reactor.run()

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/be1f2141/plugins/experimental/metalink/test/longer
----------------------------------------------------------------------
diff --git a/plugins/experimental/metalink/test/longer b/plugins/experimental/metalink/test/longer
new file mode 100755
index 0000000..f08feae
--- /dev/null
+++ b/plugins/experimental/metalink/test/longer
@@ -0,0 +1,92 @@
+#!/usr/bin/env python
+
+#  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.
+
+print '''1..1 longer
+# The proxy doesn't choke if the server sends more content than it advertised'''
+
+# Unlike the contentLength test, don't stop the reactor at the end of the
+# headers.  Give the proxy time to choke.
+
+from twisted.internet import error, protocol, reactor, tcp
+from twisted.web import http
+
+def callback():
+  print 'not ok 1 - No Content-Length header'
+
+  reactor.stop()
+
+reactor.callLater(1, callback)
+
+class factory(http.HTTPFactory):
+  class protocol(http.HTTPChannel):
+    class requestFactory(http.Request):
+      def requestReceived(ctx, method, target, version):
+
+        ctx.client = None
+        ctx.clientproto = version
+
+        ctx.setHeader('Content-Length', 1)
+        ctx.write('longer')
+
+server = tcp.Port(0, factory())
+server.startListening()
+
+print '# Listening on {0}:{1}'.format(*server.socket.getsockname())
+
+class factory(protocol.ClientFactory):
+  def clientConnectionFailed(ctx, connector, reason):
+
+    print 'Bail out!'
+    reason.printTraceback()
+
+    reactor.stop()
+
+  class protocol(http.HTTPClient):
+    def connectionLost(ctx, reason):
+      try:
+        reactor.stop()
+
+      except error.ReactorNotRunning:
+        pass
+
+      else:
+        print 'not ok 1 - Did the proxy crash?  (The client connection closed.)'
+
+    connectionMade = lambda ctx: ctx.transport.write('GET {0}:{1} HTTP/1.1\r\n\r\n'.format(*server.socket.getsockname()))
+
+    def handleHeader(ctx, k, v):
+      if k.lower() == 'content-length':
+        if v != '1':
+          print 'not',
+
+        print 'ok 1 - Content-Length header'
+
+        reactor.stop()
+
+    # Avoid calling undefined handleResponse() at the end of the content.
+    # (Override connectionLost() when the proxy crashes or stop the reactor.)
+    #
+    # The data that was already received will be processed (the end of the
+    # headers), then shutdown events will fire (connections will be closed),
+    # and then finally the reactor will grind to a halt.
+    def handleResponseEnd(ctx):
+      pass
+
+tcp.Connector('localhost', 8080, factory(), 30, None, reactor).connect()
+
+reactor.run()

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/be1f2141/plugins/experimental/metalink/test/notModified
----------------------------------------------------------------------
diff --git a/plugins/experimental/metalink/test/notModified b/plugins/experimental/metalink/test/notModified
new file mode 100755
index 0000000..2854189
--- /dev/null
+++ b/plugins/experimental/metalink/test/notModified
@@ -0,0 +1,77 @@
+#!/usr/bin/env python
+
+#  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.
+
+print '''1..2 notModified
+# The proxy doesn't crash on a 304 Not Modified response'''
+
+from twisted.internet import error, protocol, reactor, tcp
+from twisted.web import http
+
+def callback():
+  print 'ok 2 - The proxy didn\'t crash (the client connection didn\'t close yet)'
+
+  reactor.stop()
+
+reactor.callLater(1, callback)
+
+class factory(http.HTTPFactory):
+  class protocol(http.HTTPChannel):
+    class requestFactory(http.Request):
+      def requestReceived(ctx, method, target, version):
+
+        ctx.client = None
+        ctx.clientproto = version
+
+        ctx.setResponseCode(304)
+        ctx.finish()
+
+server = tcp.Port(0, factory())
+server.startListening()
+
+print '# Listening on {0}:{1}'.format(*server.socket.getsockname())
+
+class factory(protocol.ClientFactory):
+  def clientConnectionFailed(ctx, connector, reason):
+
+    print 'Bail out!'
+    reason.printTraceback()
+
+    reactor.stop()
+
+  class protocol(http.HTTPClient):
+    def connectionLost(ctx, reason):
+      try:
+        reactor.stop()
+
+      except error.ReactorNotRunning:
+        pass
+
+      else:
+        print 'not ok 1 - Did the proxy crash?  (The client connection closed.)'
+
+    connectionMade = lambda ctx: ctx.transport.write('GET {0}:{1} HTTP/1.1\r\n\r\n'.format(*server.socket.getsockname()))
+
+    def handleStatus(ctx, version, status, message):
+      if status != '304':
+        print 'not',
+
+      print 'ok 1 - 304 Not Modified response status'
+
+tcp.Connector('localhost', 8080, factory(), 30, None, reactor).connect()
+
+reactor.run()

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/be1f2141/plugins/experimental/metalink/test/shortChunkedEncodingDisconnect
----------------------------------------------------------------------
diff --git a/plugins/experimental/metalink/test/shortChunkedEncodingDisconnect b/plugins/experimental/metalink/test/shortChunkedEncodingDisconnect
new file mode 100755
index 0000000..526b233
--- /dev/null
+++ b/plugins/experimental/metalink/test/shortChunkedEncodingDisconnect
@@ -0,0 +1,96 @@
+#!/usr/bin/env python
+
+#  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.
+
+print '''1..1 shortChunkedEncodingDisconnect
+# The proxy closes the client connection and doesn't send a final chunk if the
+# server disconnects without sending one, before the proxy sends the response
+# headers'''
+
+from twisted.internet import error, protocol, reactor, tcp
+from twisted.web import http
+
+def callback():
+  print 'not ok 1 - The client was left hanging'
+
+  reactor.stop()
+
+reactor.callLater(1, callback)
+
+class factory(http.HTTPFactory):
+  class protocol(http.HTTPChannel):
+    class requestFactory(http.Request):
+      def requestReceived(ctx, method, target, version):
+
+        ctx.client = None
+        ctx.clientproto = version
+
+        ctx.write('shortChunkedEncodingDisconnect')
+
+        # Disconnect before the proxy sends the response headers
+        ctx.transport.loseConnection()
+
+server = tcp.Port(0, factory())
+server.startListening()
+
+print '# Listening on {0}:{1}'.format(*server.socket.getsockname())
+
+class factory(protocol.ClientFactory):
+  def clientConnectionFailed(ctx, connector, reason):
+
+    print 'Bail out!'
+    reason.printTraceback()
+
+    reactor.stop()
+
+  class protocol(http.HTTPClient):
+    def connectionLost(ctx, reason):
+      try:
+        reactor.stop()
+
+      except error.ReactorNotRunning:
+        pass
+
+      else:
+        print 'ok 1 - The client connection closed'
+
+    connectionMade = lambda ctx: ctx.transport.write('GET {0}:{1} HTTP/1.1\r\n\r\n'.format(*server.socket.getsockname()))
+
+    def handleHeader(ctx, k, v):
+      if k.lower() == 'content-length':
+        print 'not ok 1 - Got a Content-Length header vs. a chunked response'
+
+        # Who cares what happens now?
+        reactor.stop()
+
+    # Avoid calling undefined handleResponse() at the end of the content (if
+    # the proxy sent a Content-Length header vs. a chunked response).
+    # (Override connectionLost() when the proxy closes the client connection or
+    # stop the reactor.)
+    def handleResponseEnd(ctx):
+      pass
+
+    def handleResponsePart(ctx, data):
+      if data.endswith('0\r\n\r\n'):
+        print 'not ok 1 - Got a final chunk'
+
+        # Who cares what happens now?
+        reactor.stop()
+
+tcp.Connector('localhost', 8080, factory(), 30, None, reactor).connect()
+
+reactor.run()

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/be1f2141/plugins/experimental/metalink/test/shortClientDisconnect
----------------------------------------------------------------------
diff --git a/plugins/experimental/metalink/test/shortClientDisconnect b/plugins/experimental/metalink/test/shortClientDisconnect
new file mode 100755
index 0000000..4ab6c83
--- /dev/null
+++ b/plugins/experimental/metalink/test/shortClientDisconnect
@@ -0,0 +1,90 @@
+#!/usr/bin/env python
+
+#  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.
+
+print '''1..1 shortClientDisconnect
+# The proxy doesn't crash if the client disconnects before the proxy sends the
+# response headers'''
+
+from twisted.internet import error, protocol, reactor, tcp
+from twisted.web import http
+
+def callback():
+  print 'not ok 1 - Why didn\'t the test finish yet?'
+
+  reactor.stop()
+
+reactor.callLater(3, callback)
+
+class factory(http.HTTPFactory):
+  class protocol(http.HTTPChannel):
+    class requestFactory(http.Request):
+      def requestReceived(ctx, method, target, version):
+
+        ctx.client = None
+        ctx.clientproto = version
+
+        ctx.write('shortClientDisconnect0')
+
+        def callback():
+          ctx.write('shortClientDisconnect1')
+
+          # Open another connection
+          class factory(protocol.ClientFactory):
+            def clientConnectionFailed(ctx, connector, reason):
+              print 'not ok 1 - Did the proxy crash?  (Can\'t open another connection to it.)'
+
+              reactor.stop()
+
+            class protocol(protocol.Protocol):
+              def connectionMade(ctx):
+                print 'ok 1 - The proxy didn\'t crash (opened another connection to it)'
+
+                reactor.stop()
+
+          reactor.callLater(1, tcp.Connector('localhost', 8080, factory(), 30, None, reactor).connect)
+
+        reactor.callLater(1, callback)
+
+server = tcp.Port(0, factory())
+server.startListening()
+
+print '# Listening on {0}:{1}'.format(*server.socket.getsockname())
+
+class factory(protocol.ClientFactory):
+  def clientConnectionFailed(ctx, connector, reason):
+
+    print 'Bail out!'
+    reason.printTraceback()
+
+    reactor.stop()
+
+  class protocol(http.HTTPClient):
+    def connectionMade(ctx):
+      ctx.transport.write('GET {0}:{1} HTTP/1.1\r\n\r\n'.format(*server.socket.getsockname()))
+
+      # Disconnect before the proxy sends the response headers
+      ctx.transport.loseConnection()
+
+    # Avoid calling undefined handleResponse() at the end of the content or
+    # when the connection closes
+    def handleResponseEnd(ctx):
+      pass
+
+tcp.Connector('localhost', 8080, factory(), 30, None, reactor).connect()
+
+reactor.run()

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/be1f2141/plugins/experimental/metalink/test/shortContentLengthDisconnect
----------------------------------------------------------------------
diff --git a/plugins/experimental/metalink/test/shortContentLengthDisconnect b/plugins/experimental/metalink/test/shortContentLengthDisconnect
new file mode 100755
index 0000000..2e1c813
--- /dev/null
+++ b/plugins/experimental/metalink/test/shortContentLengthDisconnect
@@ -0,0 +1,93 @@
+#!/usr/bin/env python
+
+#  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.
+
+print '''1..2 shortContentLengthDisconnect
+# The proxy sends the right Content-Length header and closes the client
+# connection if the server disconnects before the proxy sends the response
+# headers'''
+
+from twisted.internet import error, protocol, reactor, tcp
+from twisted.web import http
+
+def callback():
+  print 'not ok 2 - The client was left hanging'
+
+  reactor.stop()
+
+reactor.callLater(1, callback)
+
+class factory(http.HTTPFactory):
+  class protocol(http.HTTPChannel):
+    class requestFactory(http.Request):
+      def requestReceived(ctx, method, target, version):
+
+        ctx.client = None
+        ctx.clientproto = version
+
+        ctx.setHeader('Content-Length', 29)
+        ctx.write('shortContentLengthDisconnect')
+
+        # Disconnect before the proxy sends the response headers
+        ctx.transport.loseConnection()
+
+server = tcp.Port(0, factory())
+server.startListening()
+
+print '# Listening on {0}:{1}'.format(*server.socket.getsockname())
+
+class factory(protocol.ClientFactory):
+  def clientConnectionFailed(ctx, connector, reason):
+
+    print 'Bail out!'
+    reason.printTraceback()
+
+    reactor.stop()
+
+  class protocol(http.HTTPClient):
+    def connectionLost(ctx, reason):
+      try:
+        reactor.stop()
+
+      except error.ReactorNotRunning:
+        pass
+
+      else:
+        print 'ok 2 - The client connection closed'
+
+    connectionMade = lambda ctx: ctx.transport.write('GET {0}:{1} HTTP/1.1\r\n\r\n'.format(*server.socket.getsockname()))
+
+    def handleHeader(ctx, k, v):
+      if k.lower() == 'content-length':
+        if v != '29':
+          print 'not',
+
+          # Who cares what happens now?
+          reactor.stop()
+
+        print 'ok 1 - Content-Length header'
+
+    # Avoid calling undefined handleResponse() at the end of the content (if
+    # the proxy sent the wrong Content-Length header).  (Override
+    # connectionLost() when the proxy closes the client connection or stop the
+    # reactor.)
+    def handleResponseEnd(ctx):
+      pass
+
+tcp.Connector('localhost', 8080, factory(), 30, None, reactor).connect()
+
+reactor.run()

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/be1f2141/plugins/experimental/metalink/test/zero
----------------------------------------------------------------------
diff --git a/plugins/experimental/metalink/test/zero b/plugins/experimental/metalink/test/zero
new file mode 100755
index 0000000..6d42d56
--- /dev/null
+++ b/plugins/experimental/metalink/test/zero
@@ -0,0 +1,90 @@
+#!/usr/bin/env python
+
+#  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.
+
+print '''1..1 zero
+# The proxy doesn't crash if the Content-Length is zero'''
+
+from twisted.internet import error, protocol, reactor, tcp
+from twisted.web import http
+
+def callback():
+  print 'not ok 1 - Why didn\'t the test finish yet?'
+
+  reactor.stop()
+
+reactor.callLater(1, callback)
+
+class factory(http.HTTPFactory):
+  class protocol(http.HTTPChannel):
+    class requestFactory(http.Request):
+      def requestReceived(ctx, method, target, version):
+
+        ctx.client = None
+        ctx.clientproto = version
+
+        ctx.setHeader('Content-Length', 0)
+        ctx.finish()
+
+server = tcp.Port(0, factory())
+server.startListening()
+
+print '# Listening on {0}:{1}'.format(*server.socket.getsockname())
+
+class factory(protocol.ClientFactory):
+  def clientConnectionFailed(ctx, connector, reason):
+
+    print 'Bail out!'
+    reason.printTraceback()
+
+    reactor.stop()
+
+  class protocol(http.HTTPClient):
+    def connectionLost(ctx, reason):
+      try:
+        reactor.stop()
+
+      except error.ReactorNotRunning:
+        pass
+
+      else:
+        print 'not ok 1 - Did the proxy crash?  (The client connection closed.)'
+
+    connectionMade = lambda ctx: ctx.transport.write('GET {0}:{1} HTTP/1.1\r\n\r\n'.format(*server.socket.getsockname()))
+
+    def handleEndHeaders(ctx):
+      try:
+        reactor.stop()
+
+      except error.ReactorNotRunning:
+        pass
+
+      else:
+        print 'not ok 1 - No Content-Length header'
+
+    def handleHeader(ctx, k, v):
+      if k.lower() == 'content-length':
+        if v != '0':
+          print 'not',
+
+        print 'ok 1 - Content-Length header'
+
+        reactor.stop()
+
+tcp.Connector('localhost', 8080, factory(), 30, None, reactor).connect()
+
+reactor.run()