aio-libs/aiohttp · error · InvalidURLError

{path}

Error message

{path}

What it means

Raised as InvalidURLError (a BadHttpMessage subclass, HTTP 400) when the request-line path is not in origin-form ('/...'), asterisk-form ('*' with OPTIONS), or authority-form (only legal with CONNECT), and is not an absolute URI. Per RFC 9112 §3.2.3, the authority-form (host:port) is allowed ONLY with the CONNECT method. Any other method using authority-form, or a malformed non-absolute path, trips this guard.

Solutions

  1. Send the request in origin-form (path beginning with '/') and put the host in the Host header, e.g. 'GET /path HTTP/1.1' with 'Host: example.com'.
  2. If you need authority-form, use the CONNECT method (it is the only method allowed to use it).
  3. If this is a proxy server, ensure the request-target is an absolute URI (http(s)://host/path) so url.absolute is True.
  4. For raw-socket tests, validate the request-line against RFC 9112 §3.2 before sending.

Example fix

// before (wrong target form)
GET example.com:443 HTTP/1.1\r\n\r\n

// after (origin-form + Host header)
GET / HTTP/1.1\r\nHost: example.com\r\n\r\n

// CONNECT is the only method allowed in authority-form
CONNECT example.com:443 HTTP/1.1\r\nHost: example.com\r\n\r\n
Defensive patterns

Strategy: validation

Validate before calling

import re
from yarl import URL

def valid_request_target(method: str, path: str) -> bool:
    # origin-form
    if path.startswith('/'):
        return True
    # asterisk-form
    if path == '*' and method.upper() == 'OPTIONS':
        return True
    # authority-form only with CONNECT
    if method.upper() == 'CONNECT':
        return bool(re.fullmatch(r'[A-Za-z0-9.\-]+:\d+', path))
    # absolute-form (proxy)
    try:
        return URL(path, encoded=True).absolute
    except Exception:
        return False

Try / catch

from aiohttp.http_exceptions import InvalidURLError, BadHttpMessage

try:
    msg, payload, _ = parser.feed_data(raw)
except InvalidURLError as e:
    # 400 Bad Request to the peer; log the malformed target
    respond_400(f'Invalid request target: {e.args[0] if e.args else ""}')
except BadHttpMessage:
    respond_400('Malformed request')

Prevention

When it happens

Trigger: A client sends a request like 'GET example.com:443 HTTP/1.1' (authority-form with a non-CONNECT method), or 'GET foo HTTP/1.1' where 'foo' is neither a path starting with '/', nor '*', nor a valid absolute URL. Constructed by HttpRequestParser.parse_message when URL(path, encoded=True).absolute is False and the method is not CONNECT.

Common situations: Misconfigured HTTP client sending proxy-style requests (absolute URI or authority) to a non-proxy server; a proxy or load balancer forwarding the wrong request-target form; a malformed test harness (e.g. raw socket test) sending an invalid request line; curl used with '--resolve' or proxy options against an origin server.

Related errors


AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11). Data as JSON: /api/errors/74c5144abf08e4d0. Report an issue: GitHub.

Appendix: source

Thrown at aiohttp/http_parser.py:704

            # NOTE: HTTP Request-Line input producing different
            # NOTE: `yarl.URL()` objects
            url = URL.build(
                path=path_part,
                query_string=qs_part,
                fragment=url_fragment,
                encoded=True,
            )
        elif path == "*" and method == "OPTIONS":
            # asterisk-form,
            url = URL(path, encoded=True)
        else:
            # absolute-form for proxy maybe,
            # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2
            url = URL(path, encoded=True)
            if not url.absolute:
                # authority-form is only allowed with CONNECT
                # https://www.rfc-editor.org/info/rfc9112/#section-3.2.3-1
                raise InvalidURLError(
                    path.encode(errors="surrogateescape").decode("latin1")
                )

        # read headers
        (
            headers,
            raw_headers,
            close,
            compression,
            upgrade,
            chunked,
        ) = self.parse_headers(lines[1:])

        if version_o == HttpVersion11 and hdrs.HOST not in headers:
            raise BadHttpMessage("Missing 'Host' header in request.")

        if close is None:  # then the headers weren't set in the request
            if version_o <= HttpVersion10:  # HTTP 1.0 must asks to not close

View on GitHub (pinned to d041d4d0fd)