aio-libs/aiohttp · error · ClientError
Digest auth error: Unsupported Quality of Protection (qop)…
Error message
Digest auth error: Unsupported Quality of Protection (qop) value(s): {qop_raw} What it means
Raised when the Digest challenge includes a 'qop' (Quality of Protection) directive but none of its comma-separated values are the RFC-supported tokens 'auth' or 'auth-int'. aiohttp only implements those two qop values; any other token (e.g. 'auth-conf') causes ClientError in _encode(). If qop is absent entirely the code skips this check and uses no qop, so the error implies qop was present but unusable.
Solutions
- Inspect the WWW-Authenticate header to see the exact qop value offered.
- If the server controls are yours, set qop to 'auth' or 'auth-int' (or omit qop to use legacy mode).
- If the server requires auth-conf, you need a different client library that supports it; aiohttp cannot.
- Confirm the qop token has no stray whitespace/quotes that would break parsing (the parser strips, but verify).
Example fix
# before — server: qop="auth-conf" await session.get(url) # ClientError # after — server offers a supported qop # WWW-Authenticate: Digest realm="x", nonce="...", qop="auth"
Defensive patterns
Strategy: validation
Validate before calling
import re
SUPPORTED_QOP = {'auth', 'auth-int'}
def server_qop_supported(www_authenticate: str) -> bool:
m = re.search(r'qop="([^"]*)"', www_authenticate, re.I)
if not m:
return True # absent qop is fine (legacy mode)
offered = {q.strip() for q in m.group(1).split(',')}
return bool(offered & SUPPORTED_QOP) Type guard
def qop_is_supported(qop_raw: str) -> bool:
if not qop_raw:
return True
offered = {q.strip() for q in qop_raw.split(',')}
return bool(offered & {'auth', 'auth-int'}) Try / catch
from aiohttp import ClientError
try:
resp = await session.get(url)
except ClientError as e:
if 'qop' in str(e).lower():
log.warning('Server qop unsupported by aiohttp; switch server config to auth/auth-int')
raise Prevention
- Standardize server-side qop on 'auth' or 'auth-int'.
- Avoid 'auth-conf' — aiohttp does not implement message confidentiality.
- Document the supported qop set wherever Digest is configured.
When it happens
Trigger: Server sends 'WWW-Authenticate: Digest ... qop="auth-conf"' (or any value other than auth/auth-int). The intersection of offered qops with {'auth','auth-int'} is empty, so the middleware raises before computing the response digest.
Common situations: Server advertising 'auth-conf' (confidentiality) which aiohttp does not implement; typo'd or custom qop tokens; legacy or non-standard Digest servers; fuzz-testing that injects arbitrary qop values.
Related errors
- Digest auth error: Unsupported hash algorithm
- Malformed Digest auth challenge: Missing 'nonce' parameter
- Malformed Digest auth challenge: Missing 'realm' parameter
- Security issue: Digest auth challenge contains empty…
- A ":" is not allowed in username (RFC 1945#section-11.1)
AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11).
Data as JSON: /api/errors/3dd888deb00e2af6.
Report an issue: GitHub.
Appendix: source
Thrown at aiohttp/client_middleware_digest_auth.py:285
# Convert string values to bytes once
nonce_bytes = nonce.encode("utf-8")
realm_bytes = realm.encode("utf-8")
# Use the encoded request-target (raw_path_qs) since that is what is
# transmitted on the wire and what the server signs against. Using the
# decoded form would cause digest verification to fail when the path
# or query string contains percent-encoded reserved characters.
path = URL(url).raw_path_qs
# Process QoP
qop = ""
qop_bytes = b""
if qop_raw:
valid_qops = {"auth", "auth-int"}.intersection(
{q.strip() for q in qop_raw.split(",") if q.strip()}
)
if not valid_qops:
raise ClientError(
f"Digest auth error: Unsupported Quality of Protection (qop) value(s): {qop_raw}"
)
qop = "auth-int" if "auth-int" in valid_qops else "auth"
qop_bytes = qop.encode("utf-8")
if algorithm not in DigestFunctions:
raise ClientError(
f"Digest auth error: Unsupported hash algorithm: {algorithm}. "
f"Supported algorithms: {', '.join(SUPPORTED_ALGORITHMS)}"
)
hash_fn: Final = DigestFunctions[algorithm]
def H(x: bytes) -> bytes:
"""RFC 7616 Section 3: Hash function H(data) = hex(hash(data))."""
return hash_fn(x).hexdigest().encode()
def KD(s: bytes, d: bytes) -> bytes:View on GitHub (pinned to d041d4d0fd)