From: Eric Wong Date: Fri, 11 Apr 2025 01:38:34 +0000 (+0000) Subject: http: send `100 Continue' responses for uploads X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=2f7bc8debb6290d5e2dad2e0d1a49f3d1c71b818;p=thirdparty%2Fpublic-inbox.git http: send `100 Continue' responses for uploads 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. --- diff --git a/lib/PublicInbox/HTTP.pm b/lib/PublicInbox/HTTP.pm index 7bee0b0a0..407f476b3 100644 --- a/lib/PublicInbox/HTTP.pm +++ b/lib/PublicInbox/HTTP.pm @@ -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; diff --git a/t/httpd-corner.t b/t/httpd-corner.t index 18518b38f..2ff40c54f 100644 --- a/t/httpd-corner.t +++ b/t/httpd-corner.t @@ -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;