Answer Expect: 100-continue before anything reads the body - #208
Merged
Conversation
A client sending "Expect: 100-continue" holds its body back until the server says it will read one. httpd answered neither 100 nor 417, so the client waited -- and the reader on this side called recv() on a socket that was empty precisely because of that wait. Sockets are non-blocking, so the call returned EWOULDBLOCK at once and the request died in under a tenth of a second with no byte of the body sent. The answer goes at the end of http_in() because that is the single point preceding every body read in the server: httppars() consumes a POST body under CBUFSIZE itself, a CGI reads a PUT body off the socket. It also comes after the 414/505/400 validation, so a rejected request draws no interim response. Emitted as one http_printf() rather than through http_resp(): httpstat() carries no phrase for 100 and would answer it with 500, and http_resp() sets httpc->resp, which httpprtv() reads when deciding whether a bare CRLF ends the final response's headers. Fixes #207
This was referenced Aug 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #207.
Answers
Expect: 100-continueat the end ofhttp_in(), so a client that isholding its body back is released before anything tries to read it.
Placement
The constraint is that it must precede every body read in the server, and
there are two:
httppars()consumes a POST body underCBUFSIZE(4000) bytesitself into
POST_STRING, and a CGI reads a PUT body straight off the socket.The end of
http_in()precedes both, so no route type added later can forgetit. It also sits after the 414/505/400 request validation, so a request httpd is
about to reject never draws an interim response.
It does mean the expectation is answered before the auth gate runs, so a client
whose request will be refused still uploads its body. That is exactly what it
does today — after waiting out its own timeout first — so nothing gets worse,
and moving the call past the gate would put it after
httppars()has alreadyread the POST body it was supposed to unblock.
Why not http_resp()
http_resp(httpc, 100)fails twice over.httpstat()carries no phrase for100, so
httpresp()would substitute500 Internal Server Error(and WTOHTTPD054E); and it setshttpc->resp, which is whathttpprtv()tests whendeciding whether a bare
\r\nterminates the final response's headers —setting it here would change how the real response gets framed. One
http_printf()of the whole 25-byte interim response touches neither.Guarded on HTTP/1.1 (RFC 7231 §5.1.1 — 1.0 has no
Expect), on the value being100-continue(caseless, viahttp_cmp), and on a body actually beingannounced.
Verification
Downstream: mvslovers/mvsmf#313 carries a curl regression case that sends
Expect: 100-continueexplicitly. Against a server without this change it costscurl its full 1 s
expect100-timeoutbefore the body goes out; against one withit, the body follows the interim response immediately. Both need a deployed
HTTPD to measure, so the live check is still outstanding — the change builds
clean and is unmeasured on MVS so far.
Two things checked rather than assumed
The env name resolves.
httpshen()builds"HTTP_%s"from the header nameas the client spelled it, so
Expect:is stored asHTTP_Expect, notHTTP_EXPECT. The lookup survives that only becausehttp_find_env()compareswith the caseless
http_cmp()— checked inhttpfenv.c, not inferred from theexisting
HTTP_CONTENT-LENGTHcall sites. Had it beenstrcmp, this PR wouldhave been a no-op that builds clean and reviews fine.
Answering before the auth gate does not strand a rejected upload. The worry
is real in shape: after this change a client with bad credentials gets 100,
starts sending, and ~1 ms later
auth_gate()answers 401 whilenodata:hasalready cleared
keepalive, so the socket closes with a large body in flight.Simulated against a listener that reproduces exactly that sequence, 1.44 MB body:
401, 0.002 s401, 0.211 s401, 1.011 s401, 3.003 scurl reads the response it already has rather than reporting the write error, in
every variant. So the early placement stands and the two-point alternative
(after
auth_gatefor PUT, insidepostdatafor POST) is not worth its extrasurface.
Verified on MVS
Built clean from this branch, deployed and activated in the running STC.
The interim response goes out, ahead of both the route lookup and the auth
gate, exactly as placed:
On the wire it is the intended 25 bytes and nothing else —
b'HTTP/1.1 100 Continue\r\n\r\n'— sohttpc->respandhttpprtv()'sheader-terminator detection are untouched and the final response frames
normally.
What it cost the client before, and what it costs now (mvsmf#313's regression
case, a two-line body with an explicit
Expect: 100-continue):ExpectExpectitselfThe 1 s that disappears is curl's
expect100-timeoutrunning out becausenobody answered.
The auth-failure path holds up on the real server too, which is the part the
local simulation could only approximate — a client now starts sending
immediately and
auth_gate()answers underneath it withkeepalivealreadycleared:
404, 0.072 s, curl exit 0401, 0.038 s, curl exit 0401, 0.071 s, curl exit 0A raw-socket probe does draw a
ConnectionResetErrorhere, but that is anartefact of the probe: it printed between reading the interim response and
sending the body, long enough for the server to answer 404 and close first. No
real client inserts that pause, and curl reports the status cleanly in all three
rows.
Regression, all against the deployed HTTPD:
curl-datasets.sh195/195,curl-binary.sh10/10,curl-uss.sh128/128,curl-jobs.sh111/111 + 2skipped,
curl-console.sh64/64.