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

  1. Inspect the WWW-Authenticate header to see the exact qop value offered.
  2. If the server controls are yours, set qop to 'auth' or 'auth-int' (or omit qop to use legacy mode).
  3. If the server requires auth-conf, you need a different client library that supports it; aiohttp cannot.
  4. 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

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


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)