{"id":"9cfaecb29b655f34","repo":"encode/httpx","slug":"unexpected-qop-value-qop-r-in-digest-auth","errorCode":null,"errorMessage":"Unexpected qop value \"{qop!r}\" in digest auth","messagePattern":"Unexpected qop value \"(.+?)\" in digest auth","errorType":"exception","errorClass":"ProtocolError","httpStatus":null,"severity":"error","filePath":"httpx/_auth.py","lineNumber":340,"sourceCode":"                if field not in NON_QUOTED_FIELDS\n                else NON_QUOTED_TEMPLATE\n            )\n            header_value += template.format(field, to_str(value))\n\n        return header_value\n\n    def _resolve_qop(self, qop: bytes | None, request: Request) -> bytes | None:\n        if qop is None:\n            return None\n        qops = re.split(b\", ?\", qop)\n        if b\"auth\" in qops:\n            return b\"auth\"\n\n        if qops == [b\"auth-int\"]:\n            raise NotImplementedError(\"Digest auth-int support is not yet implemented\")\n\n        message = f'Unexpected qop value \"{qop!r}\" in digest auth'\n        raise ProtocolError(message, request=request)\n\n\nclass _DigestAuthChallenge(typing.NamedTuple):\n    realm: bytes\n    nonce: bytes\n    algorithm: str\n    opaque: bytes | None\n    qop: bytes | None\n","sourceCodeStart":322,"sourceCodeEnd":349,"githubUrl":"https://github.com/encode/httpx/blob/b5addb64f0161ff6bfe94c124ef76f6a1fba5254/httpx/_auth.py#L322-L349","documentation":"Raised as httpx.ProtocolError by DigestAuth._resolve_qop when the server's qop value is present but contains neither 'auth' nor 'auth-int'. The split list does not match any qop value httpx can negotiate.","triggerScenarios":"A 401 Digest challenge with an unrecognized qop token, e.g. 'Digest ... qop=\"foobar\"' or a malformed 'qop=\"\"'. _resolve_qop finds no 'auth' and the list is not exactly ['auth-int'].","commonSituations":"Non-compliant servers with typos in qop; proprietary qop extensions; corrupted or truncated WWW-Authenticate headers.","solutions":["Capture the 401 WWW-Authenticate header to confirm the invalid qop value.","Do not use httpx.DigestAuth against that endpoint; use a custom Auth or token.","Fix the server's Digest configuration to advertise qop=auth."],"exampleFix":"// before\nclient.get(url, auth=httpx.DigestAuth(\"u\", \"p\"))\n// after\ndef auth_flow(req):\n    req.headers[\"Authorization\"] = \"Bearer <token>\"\n    yield req\nclient.get(url, auth=auth_flow)","handlingStrategy":"try-catch","validationCode":"import re\nprobe = client.get(url)\nfor h in (probe.headers.get_list(\"www-authenticate\") if probe.status_code == 401 else []):\n    if h.lower().startswith(\"digest \"):\n        m = re.search(r'qop=\"?([^\"]+)\"?', h)\n        if m:\n            qops = [q.strip() for q in m.group(1).split(\",\")]\n            negotiable = \"auth\" in qops or qops == [\"auth-int\"]\n            # if not negotiable, DigestAuth will raise ProtocolError","typeGuard":"def digest_qop_negotiable(www_auth_values: list[str]) -> bool:\n    import re\n    for h in www_auth_values:\n        if h.lower().startswith(\"digest \"):\n            m = re.search(r'qop=\"?([^\"]+)\"?', h)\n            if m:\n                qops = [q.strip() for q in m.group(1).split(\",\")]\n                return \"auth\" in qops or qops == [\"auth-int\"]\n    return True","tryCatchPattern":"try:\n    resp = client.get(url, auth=httpx.DigestAuth(user, pw))\nexcept httpx.ProtocolError as exc:\n    log.warning(\"Unrecognized digest qop: %s\", exc)\n    resp = client.get(url, headers={\"Authorization\": \"Bearer <token>\"})","preventionTips":["Validate the qop value advertised by the server before using DigestAuth.","Fall back to a non-digest scheme when the server's qop is unrecognized.","Catch httpx.ProtocolError to keep callers resilient."],"tags":["digest-auth","authentication","protocol"],"analyzedSha":"b5addb64f0161ff6bfe94c124ef76f6a1fba5254","analyzedAt":"2026-08-04T19:32:56.768Z","schemaVersion":2}