{"record":{"id":"3dd888deb00e2af6","repo":"aio-libs/aiohttp","slug":"digest-auth-error-unsupported-quality-of-protecti","errorCode":null,"errorMessage":"Digest auth error: Unsupported Quality of Protection (qop) value(s): {qop_raw}","messagePattern":"Digest auth error: Unsupported Quality of Protection \\(qop\\) value\\(s\\): (.+?)","errorType":"exception","errorClass":"ClientError","httpStatus":null,"severity":"error","filePath":"aiohttp/client_middleware_digest_auth.py","lineNumber":285,"sourceCode":"\n        # Convert string values to bytes once\n        nonce_bytes = nonce.encode(\"utf-8\")\n        realm_bytes = realm.encode(\"utf-8\")\n        # Use the encoded request-target (raw_path_qs) since that is what is\n        # transmitted on the wire and what the server signs against. Using the\n        # decoded form would cause digest verification to fail when the path\n        # or query string contains percent-encoded reserved characters.\n        path = URL(url).raw_path_qs\n\n        # Process QoP\n        qop = \"\"\n        qop_bytes = b\"\"\n        if qop_raw:\n            valid_qops = {\"auth\", \"auth-int\"}.intersection(\n                {q.strip() for q in qop_raw.split(\",\") if q.strip()}\n            )\n            if not valid_qops:\n                raise ClientError(\n                    f\"Digest auth error: Unsupported Quality of Protection (qop) value(s): {qop_raw}\"\n                )\n\n            qop = \"auth-int\" if \"auth-int\" in valid_qops else \"auth\"\n            qop_bytes = qop.encode(\"utf-8\")\n\n        if algorithm not in DigestFunctions:\n            raise ClientError(\n                f\"Digest auth error: Unsupported hash algorithm: {algorithm}. \"\n                f\"Supported algorithms: {', '.join(SUPPORTED_ALGORITHMS)}\"\n            )\n        hash_fn: Final = DigestFunctions[algorithm]\n\n        def H(x: bytes) -> bytes:\n            \"\"\"RFC 7616 Section 3: Hash function H(data) = hex(hash(data)).\"\"\"\n            return hash_fn(x).hexdigest().encode()\n\n        def KD(s: bytes, d: bytes) -> bytes:","sourceCodeStart":267,"sourceCodeEnd":303,"githubUrl":"https://github.com/aio-libs/aiohttp/blob/d041d4d0fd48c3f0832084d33be16cf1c4835f85/aiohttp/client_middleware_digest_auth.py#L267-L303","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"# before — server: qop=\"auth-conf\"\nawait session.get(url)  # ClientError\n\n# after — server offers a supported qop\n# WWW-Authenticate: Digest realm=\"x\", nonce=\"...\", qop=\"auth\"","handlingStrategy":"validation","validationCode":"import re\n\nSUPPORTED_QOP = {'auth', 'auth-int'}\n\ndef server_qop_supported(www_authenticate: str) -> bool:\n    m = re.search(r'qop=\"([^\"]*)\"', www_authenticate, re.I)\n    if not m:\n        return True  # absent qop is fine (legacy mode)\n    offered = {q.strip() for q in m.group(1).split(',')}\n    return bool(offered & SUPPORTED_QOP)","typeGuard":"def qop_is_supported(qop_raw: str) -> bool:\n    if not qop_raw:\n        return True\n    offered = {q.strip() for q in qop_raw.split(',')}\n    return bool(offered & {'auth', 'auth-int'})","tryCatchPattern":"from aiohttp import ClientError\n\ntry:\n    resp = await session.get(url)\nexcept ClientError as e:\n    if 'qop' in str(e).lower():\n        log.warning('Server qop unsupported by aiohttp; switch server config to auth/auth-int')\n    raise","preventionTips":["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."],"tags":["digest-auth","authentication","http","client-middleware","qop"],"backgroundTag":null,"analyzedSha":"d041d4d0fd48c3f0832084d33be16cf1c4835f85","analyzedAt":"2026-08-11T20:44:15.550Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}