aio-libs/aiohttp · error · ValueError

Method cannot contain non-token characters

Error message

Method cannot contain non-token characters {method!r} (found at least {match.group()!r})

What it means

Raised by ClientRequest.__init__ when the HTTP method string contains a character outside the HTTP token set (the regex _CONTAINS_CONTROL_CHAR_RE matches anything not in [-!#$%&'*+.^_`|~0-9a-zA-Z]). This catches control characters, whitespace, and other invalid bytes that would corrupt the request line. It is a ValueError raised at request construction, before any network I/O.

Solutions

  1. Hard-code method names as literals ('GET', 'POST') instead of building from untrusted input.
  2. If accepting user input, validate against an allowlist: {'GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'}.
  3. Strip and reject any method containing whitespace or non-printable characters before passing to aiohttp.
  4. Treat this error as a possible injection attempt and log it for security review.

Example fix

# before
method = user_input  # e.g. 'GET\r\nX-Evil: 1'
await session.request(method, url)  # ValueError

# after — allowlist validation
ALLOWED = {'GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'}
if method.upper() not in ALLOWED:
    raise ValueError(f'unsupported method: {method!r}')
await session.request(method.upper(), url)
Defensive patterns

Strategy: type-guard

Validate before calling

import re
TOKEN_RE = re.compile(r"^[-!#$%&'*+.^_`|~0-9a-zA-Z]+$")
ALLOWED_METHODS = {'GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'}

def safe_method(m: str) -> str:
    m = m.upper()
    if m not in ALLOWED_METHODS or not TOKEN_RE.fullmatch(m):
        raise ValueError(f'unsafe HTTP method: {m!r}')
    return m

Type guard

import re
_TOKEN = re.compile(r"^[-!#$%&'*+.^_`|~0-9a-zA-Z]+$")
def is_valid_method(method: str) -> bool:
    return isinstance(method, str) and bool(_TOKEN.fullmatch(method))

Try / catch

try:
    await session.request(method, url)
except ValueError as e:
    if 'non-token characters' in str(e):
        raise ValueError(f'Reject method as possibly injected: {method!r}')
    raise

Prevention

When it happens

Trigger: Passing a method like 'GET\r\nX-Injected: 1' (CRLF injection attempt), 'GET ' (trailing space), 'POS\x00T' (null byte), or any method with a non-token character to session.request() / session.get() etc. The regex search in ClientRequest.__init__ matches and raises ValueError.

Common situations: User-supplied or config-driven method strings not sanitized; CRLF/header-injection attempts (security-relevant); accidental newline or trailing whitespace in a method constant; constructing methods from concatenated/templated input.

Related errors


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

Appendix: source

Thrown at aiohttp/client_reqrep.py:827

    _skip_auto_headers: "CIMultiDict[None] | None" = None

    # N.B.
    # Adding __del__ method with self._writer closing doesn't make sense
    # because _writer is instance method, thus it keeps a reference to self.
    # Until writer has finished finalizer will not be called.

    def __init__(
        self,
        method: str,
        url: URL,
        *,
        headers: CIMultiDict[str],
        loop: asyncio.AbstractEventLoop,
        ssl: SSLContext | bool | Fingerprint,
        trust_env: bool = False,
    ):
        if match := _CONTAINS_CONTROL_CHAR_RE.search(method):
            raise ValueError(
                f"Method cannot contain non-token characters {method!r} "
                f"(found at least {match.group()!r})"
            )
        # URL forbids subclasses, so a simple type check is enough.
        assert type(url) is URL, url
        self.original_url = url
        self.url = url.with_fragment(None) if url.raw_fragment else url
        self.method = method.upper()
        self.loop = loop
        self._ssl = ssl

        if loop.get_debug():
            self._source_traceback = traceback.extract_stack(sys._getframe(1))

        if not url.raw_host:
            raise InvalidURL(url)
        self._update_headers(headers)
        if url.raw_user or url.raw_password:

View on GitHub (pinned to d041d4d0fd)