diff --git a/gunicorn/http/errors.py b/gunicorn/http/errors.py index e9c24917..92e5431c 100644 --- a/gunicorn/http/errors.py +++ b/gunicorn/http/errors.py @@ -47,6 +47,14 @@ class InvalidRequestMethod(ParseException): return "Invalid HTTP method: %r" % self.method +class ExpectationFailed(ParseException): + def __init__(self, expect): + self.expect = expect + + def __str__(self): + return "Unable to comply with expectation: %r" % (self.expect, ) + + class InvalidHTTPVersion(ParseException): def __init__(self, version): self.version = version diff --git a/gunicorn/http/message.py b/gunicorn/http/message.py index 1f939c21..d12c136f 100644 --- a/gunicorn/http/message.py +++ b/gunicorn/http/message.py @@ -14,6 +14,7 @@ from gunicorn.http.errors import ( InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion, LimitRequestLine, LimitRequestHeaders, UnsupportedTransferCoding, ObsoleteFolding, + ExpectationFailed, ) from gunicorn.http.errors import InvalidProxyLine, InvalidProxyHeader, ForbiddenProxyRequest from gunicorn.http.errors import InvalidSchemeHeaders @@ -90,6 +91,7 @@ class Message: self.body = None self.scheme = "https" if cfg.is_ssl else "http" self.must_close = False + self._expected_100_continue = False # set headers limits self.limit_request_fields = cfg.limit_request_fields @@ -180,6 +182,21 @@ class Message: if header_length > self.limit_request_field_size > 0: raise LimitRequestHeaders("limit request headers fields size") + if not from_trailer and name == "EXPECT": + # https://datatracker.ietf.org/doc/html/rfc9110#section-10.1.1 + # "The Expect field value is case-insensitive." + if value.lower() == "100-continue": + if self.version < (1, 1): + # https://datatracker.ietf.org/doc/html/rfc9110#section-10.1.1-12 + # "A server that receives a 100-continue expectation + # in an HTTP/1.0 request MUST ignore that expectation." + pass + else: + self._expected_100_continue = True + # N.B. understood but ignored expect header does not return 417 + else: + raise ExpectationFailed(value) + if name in secure_scheme_headers: secure = value == secure_scheme_headers[name] scheme = "https" if secure else "http" diff --git a/gunicorn/http/wsgi.py b/gunicorn/http/wsgi.py index 419ac503..e0ff8699 100644 --- a/gunicorn/http/wsgi.py +++ b/gunicorn/http/wsgi.py @@ -117,13 +117,14 @@ def create(req, sock, client, server, cfg): host = None script_name = os.environ.get("SCRIPT_NAME", "") + if req._expected_100_continue: + sock.send(b"HTTP/1.1 100 Continue\r\n\r\n") + # rfc9112: Expect MUST be forwarded if the request is forwarded + # N.B. gunicorn just sends at most one - application might send another + # add the headers to the environ for hdr_name, hdr_value in req.headers: - if hdr_name == "EXPECT": - # handle expect - if hdr_value.lower() == "100-continue": - sock.send(b"HTTP/1.1 100 Continue\r\n\r\n") - elif hdr_name == 'HOST': + if hdr_name == 'HOST': host = hdr_value elif hdr_name == "SCRIPT_NAME": script_name = hdr_value diff --git a/gunicorn/workers/base.py b/gunicorn/workers/base.py index 93c465c9..c7a46e0e 100644 --- a/gunicorn/workers/base.py +++ b/gunicorn/workers/base.py @@ -19,7 +19,7 @@ from gunicorn.http.errors import ( InvalidProxyLine, InvalidRequestLine, InvalidRequestMethod, InvalidSchemeHeaders, LimitRequestHeaders, LimitRequestLine, - UnsupportedTransferCoding, + UnsupportedTransferCoding, ExpectationFailed, ConfigurationProblem, ObsoleteFolding, ) from gunicorn.http.wsgi import Response, default_environ @@ -212,7 +212,7 @@ class Worker: LimitRequestLine, LimitRequestHeaders, InvalidProxyLine, ForbiddenProxyRequest, InvalidSchemeHeaders, UnsupportedTransferCoding, - ConfigurationProblem, ObsoleteFolding, + ConfigurationProblem, ObsoleteFolding, ExpectationFailed, SSLError, )): @@ -239,6 +239,10 @@ class Worker: req = exc.req # for access log elif isinstance(exc, LimitRequestLine): mesg = "%s" % str(exc) + elif isinstance(exc, ExpectationFailed): + reason = "Expectation Failed" + mesg = str(exc) + status_int = 417 elif isinstance(exc, LimitRequestHeaders): reason = "Request Header Fields Too Large" mesg = "Error parsing headers: '%s'" % str(exc)