]> git.ipfire.org Git - thirdparty/public-inbox.git/commitdiff
http: send `100 Continue' responses for uploads
authorEric Wong <e@80x24.org>
Fri, 11 Apr 2025 01:38:34 +0000 (01:38 +0000)
committerEric Wong <e@80x24.org>
Fri, 11 Apr 2025 08:40:40 +0000 (08:40 +0000)
curl(1) defaults to sending a `Expect: 100-continue' header and
waits 1 second for a `100' response when using the `-T' switch
to upload via PUT.  We'll unconditionally respond with a `100'
response to save curl users 1 second.  Expecting curl users to
to disable the Expect: header via `-HExpect:' on every invocation
seems unreasonable, so our behavior should be tailored to curl
default behavior as much as possible.

We can't delegate the decision to send 100 or not to the
underlying Plack app (e.g. PublicInbox::WWW) since PSGI specs
don't provide a convenient callback interface for dealing with
trickled env->{'psgi.input'} reads.  Of course, having to
synchronously wait on env->{'psgi.input'}->read would be
unacceptable since that would prevent the server process from
servicing other concurrent clients.

lib/PublicInbox/HTTP.pm
t/httpd-corner.t

index 7bee0b0a0ea3f7bd6a75a238846455c2990551bf..407f476b315e86a0445797f00f3286bb1d0dbd01 100644 (file)
@@ -351,7 +351,14 @@ sub input_prepare {
                        $input = $null_io;
                }
        }
-
+       # curl sends `Expect: 100-continue' by default on uploads and our
+       # buffer-in-full behavior can't delegate the decision to apps, so
+       # assume we're OK with the client continuing at this point
+       if (($env->{HTTP_EXPECT} // '') =~ /\A100-continue\z/i) {
+               $self->write($env->{SERVER_PROTOCOL} .
+                               " 100 Continue\r\n\r\n");
+               $self->{sock} or return;
+       }
        # TODO: expire idle clients on ENFILE / EMFILE
        $env->{'psgi.input'} = $input // return;
        $self->{env} = $env;
index 18518b38fca0c67025ddb89dc5eea8b3b3d218ae..2ff40c54f22b9ec89f35c2c59eaeb4baae1f2833 100644 (file)
@@ -470,24 +470,30 @@ my $check_self = sub {
 SKIP: {
        my $curl = require_cmd('curl', 1) or skip('curl(1) missing', 4);
        my $url = "$base_url/sha1";
-       my ($r, $w);
-       pipe $r, $w;
-       my $cmd = [$curl, qw(--tcp-nodelay -T- -HExpect: -gsSN), $url];
-       open my $cout, '+>', undef;
-       open my $cerr, '>', undef;
-       my $rdr = { 0 => $r, 1 => $cout, 2 => $cerr };
-       my $pid = spawn($cmd, undef, $rdr);
-       close $r;
-       foreach my $c ('a'..'z') {
-               print $w $c or die "failed to write to curl: $!";
-               $delay->();
+       my $e100_timeo = 60;
+       my $t0 = now;
+       for my $H (['--expect100-timeout', $e100_timeo], ['-HExpect:']) {
+               pipe my $r, my $w;
+               my $cmd = [$curl, qw(--tcp-nodelay -T- -gsSN), @$H, $url];
+               open my $cout, '+>', undef;
+               open my $cerr, '>', undef;
+               my $rdr = { 0 => $r, 1 => $cout, 2 => $cerr };
+               my $pid = spawn($cmd, undef, $rdr);
+               close $r;
+               for my $c ('a'..'z') {
+                       print $w $c or die "failed to write to curl: $!";
+                       $delay->();
+               }
+               close $w;
+               waitpid($pid, 0);
+               is($?, 0, 'curl exited successfully');
+               is(-s $cerr, 0, 'no errors from curl');
+               seek($cout, 0, SEEK_SET);
+               is(<$cout>, sha1_hex($str), 'read expected body');
        }
-       close $w;
-       waitpid($pid, 0);
-       is($?, 0, 'curl exited successfully');
-       is(-s $cerr, 0, 'no errors from curl');
-       seek($cout, 0, SEEK_SET);
-       is(<$cout>, sha1_hex($str), 'read expected body');
+       my $elapsed = now - $t0;
+       diag "elapsed=${elapsed}s";
+       ok $elapsed < $e100_timeo, 'able to handle 100-continue responses';
 
        my $fh = popen_rd([$curl, '-gsS', "$base_url/async-big"]);
        my $n = 0;