Index: trunk/lib/Perlbal/ClientProxy.pm
===================================================================
--- trunk/lib/Perlbal/ClientProxy.pm (revision 612)
+++ trunk/lib/Perlbal/ClientProxy.pm (revision 617)
@@ -10,4 +10,6 @@
 use base "Perlbal::ClientHTTPBase";
 no  warnings qw(deprecated);
+
+use Perlbal::ChunkedUploadState;
 
 use fields (
@@ -36,4 +38,7 @@
             'backend_stalled',   # boolean:  if backend has shut off its reads because we're too slow.
             'unread_data_waiting',  # boolean:  if we shut off reads while we know data is yet to be read from client
+            'chunked_upload_state', # bool/obj:  if processing a chunked upload, Perlbal::ChunkedUploadState object, else undef
+            'request_body_length',  # integer:  request's body length, either as-declared,
+                                    #           or calculated after chunked upload is complete
 
             # for perlbal sending out UDP packets related to upload status (for xmlhttprequest upload bar)
@@ -99,4 +104,6 @@
     $self->{buoutpos} = 0;
     $self->{bureason} = undef;
+    $self->{chunked_upload_state} = undef;
+    $self->{request_body_length} = undef;
 
     $self->{reproxy_uris} = undef;
@@ -467,4 +474,6 @@
     $self->{bureason} = undef;
     $self->{upload_session} = undef;
+    $self->{chunked_upload_state} = undef;
+    $self->{request_body_length} = undef;
     return 1;
 }
@@ -569,4 +578,16 @@
         print "  disabling reads.\n" if Perlbal::DEBUG >= 3;
         $self->watch_read(0);
+        return;
+    }
+
+    # deal with chunked uploads
+    if (my $cus = $self->{chunked_upload_state}) {
+        $cus->on_readable($self);
+
+        # if we got more than 1MB not flushed to disk,
+        # stop reading for a bit until disk catches up
+        if ($self->{read_ahead} > 1024*1024) {
+            $self->watch_read(0);
+        }
         return;
     }
@@ -713,9 +734,15 @@
     return if $svc->run_hook('start_http_request',  $self);
 
-    # if defined we're waiting on some amount of data.  also, we have to
-    # subtract out read_size, which is the amount of data that was
-    # extra in the packet with the header that's part of the body.
-    $self->{content_length_remain} = $req_hd->content_length;
-    $self->{unread_data_waiting} = 1 if $self->{content_length_remain};
+    if ($self->handle_chunked_upload) {
+        # handled in method.
+    } else {
+        # if defined we're waiting on some amount of data.  also, we have to
+        # subtract out read_size, which is the amount of data that was
+        # extra in the packet with the header that's part of the body.
+        $self->{request_body_length} =
+            $self->{content_length_remain} =
+            $req_hd->content_length;
+        $self->{unread_data_waiting} = 1 if $self->{content_length_remain};
+    }
 
     # upload-tracking stuff.  both starting a new upload track session,
@@ -729,5 +756,10 @@
     # either start buffering some of the request to memory, or
     # immediately request a backend connection.
-    if ($self->{content_length_remain} && $self->{service}->{buffer_backend_connect}) {
+    if ($self->{chunked_upload_state}) {
+        $self->{request_body_length} = 0;
+        $self->{is_buffering} = 1;
+        $self->{bureason} = 'chunked';
+        $self->buffered_upload_update;
+    } elsif ($self->{content_length_remain} && $self->{service}->{buffer_backend_connect}) {
         # the deeper path
         $self->start_buffering_request;
@@ -741,4 +773,42 @@
         $self->request_backend;
     }
+}
+
+sub handle_chunked_upload {
+    my Perlbal::ClientProxy $self = shift;
+    my $req_hd = $self->{req_headers};
+    my $te = $req_hd->header("Transfer-Encoding");
+    return unless $te && $te eq "chunked";
+    return unless $self->{service}->{buffer_uploads};
+
+    $req_hd->header("Transfer-Encoding", undef); # remove it (won't go to backend)
+
+    # TODO: return false if we don't have buffered upload dir configured
+    my $eh = $req_hd->header("Expect");
+    if ($eh && $eh =~ /\b100-continue\b/) {
+        $self->write(\ "HTTP/1.1 100 Continue\r\n\r\n");
+        $req_hd->header("Expect", undef); # remove it (won't go to backend)
+    }
+
+    my $args = {
+        on_new_chunk => sub {
+            my $cref = shift;
+            my $len = length($$cref);
+            push @{$self->{read_buf}}, $cref;
+            $self->{read_ahead}          += $len;
+            $self->{request_body_length} += $len;
+            # TODO: if too large, disconnect?
+            $self->buffered_upload_update;
+        },
+        on_disconnect => sub {
+            $self->client_disconnected;
+        },
+        on_zero_chunk => sub {
+            $self->send_buffered_upload;
+        },
+    };
+
+    $self->{chunked_upload_state} = Perlbal::ChunkedUploadState->new(%$args);
+    return 1;
 }
 
@@ -915,4 +985,11 @@
 
     # make sure our buoutpos is the same as the content length...
+    return if $self->{is_writing};
+
+    # set the content-length that goes to the backend...
+    if ($self->{chunked_upload_state}) {
+        $self->{req_headers}->header("Content-Length", $self->{request_body_length});
+    }
+
     my $clen = $self->{req_headers}->content_length;
     if ($clen != $self->{buoutpos}) {
@@ -935,5 +1012,6 @@
 
     # now send the data
-    my $clen = $self->{req_headers}->content_length;
+    my $clen = $self->{request_body_length};
+
     my $sent = Perlbal::Socket::sendfile($be->{fd}, fileno($self->{bufh}), $clen - $self->{buoutpos});
     if ($sent < 0) {
@@ -1017,6 +1095,21 @@
         }
 
+        # if we're processing a chunked upload, ...
+        if ($self->{chunked_upload_state}) {
+            # turn reads back on, if we haven't hit the end yet.
+            if ($self->{unread_data_waiting} && $self->{read_ahead} < 1024*1024) {
+                $self->watch_read(1);
+                $self->{unread_data_waiting} = 0;
+            }
+
+            if ($self->{read_ahead} == 0 && $self->{chunked_upload_state}->hit_zero_chunk) {
+                $self->watch_read(0);
+                $self->send_buffered_upload;
+                return;
+            }
+        }
+
         # if we're done (no clr and no read ahead!) then send it
-        if ($self->{read_ahead} <= 0 && $self->{content_length_remain} <= 0) {
+        elsif ($self->{read_ahead} <= 0 && $self->{content_length_remain} <= 0) {
             $self->send_buffered_upload;
             return;
Index: trunk/lib/Perlbal/ChunkedUploadState.pm
===================================================================
--- trunk/lib/Perlbal/ChunkedUploadState.pm (revision 617)
+++ trunk/lib/Perlbal/ChunkedUploadState.pm (revision 617)
@@ -0,0 +1,64 @@
+package Perlbal::ChunkedUploadState;
+use strict;
+
+sub new {
+    my ($pkg, %args) = @_;
+    my $self = bless {
+        'rawbuf' => '',
+        'bytes_remain' => 0,  # remaining in chunk (ignoring final 2 byte CRLF)
+    }, $pkg;
+    foreach my $k (qw(on_new_chunk on_disconnect on_zero_chunk)) {
+        $self->{$k} = (delete $args{$k}) || sub {};
+    }
+    die "bogus args" if %args;
+    return $self;
+}
+
+sub on_readable {
+    my ($self, $ds) = @_;
+    my $rbuf = $ds->read(131072);
+    unless (defined $rbuf) {
+        $self->{on_disconnect}->();
+        return;
+    }
+
+    $self->{buf} .= $$rbuf;
+
+    while ($self->drive_machine) {}
+}
+
+# returns 1 if progress was made parsing buffer
+sub drive_machine {
+    my $self = shift;
+
+    my $buflen = length($self->{buf});
+    return 0 unless $buflen;
+
+    if (my $br = $self->{bytes_remain}) {
+        my $extract = $buflen > $br ? $br : $buflen;
+        my $ch = substr($self->{buf}, 0, $extract, '');
+        $self->{bytes_remain} -= $extract;
+        die "assert" if $self->{bytes_remain} < 0;
+        $self->{on_new_chunk}->(\$ch);
+        return 1;
+    }
+
+    return 0 unless $self->{buf} =~ s/^(?:\r\n)?([0-9a-fA-F]+)(?:;.*)?\r\n//;
+    $self->{bytes_remain} = hex($1);
+
+    if ($self->{bytes_remain} == 0) {
+        # FIXME: new state machine state for trailer parsing/discarding.
+        # (before we do on_zero_chunk).  for now, though, just assume
+        # no trailers and throw away the extra post-trailer \r\n that
+        # is probably in this packet.  hacky.
+        $self->{buf} =~ s/^\r\n//;
+        $self->{hit_zero} = 1;
+        $self->{on_zero_chunk}->();
+        return 0;
+    }
+    return 1;
+}
+
+sub hit_zero_chunk { $_[0]{hit_zero} }
+
+1;
