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
- Hard-code method names as literals ('GET', 'POST') instead of building from untrusted input.
- If accepting user input, validate against an allowlist: {'GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'}.
- Strip and reject any method containing whitespace or non-printable characters before passing to aiohttp.
- 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
- Never build HTTP methods from untrusted input; use an allowlist.
- Sanitize any config-driven method string before passing to aiohttp.
- Treat method-validation failures as security events and log them.
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
- compress must be one of True, False, 'deflate', or 'gzip'
- Invalid Content-Length header
- InvalidURL
- Bad HTTP method in status line
- chunked can not be set if Content-Length header is set
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)