{"id":"fdff53ce036779d1","repo":"encode/httpx","slug":"malformed-digest-www-authenticate-header","errorCode":null,"errorMessage":"Malformed Digest WWW-Authenticate header","messagePattern":"Malformed Digest WWW-Authenticate header","errorType":"exception","errorClass":"ProtocolError","httpStatus":null,"severity":"error","filePath":"httpx/_auth.py","lineNumber":253,"sourceCode":"        assert scheme.lower() == \"digest\"\n\n        header_dict: dict[str, str] = {}\n        for field in parse_http_list(fields):\n            key, value = field.strip().split(\"=\", 1)\n            header_dict[key] = unquote(value)\n\n        try:\n            realm = header_dict[\"realm\"].encode()\n            nonce = header_dict[\"nonce\"].encode()\n            algorithm = header_dict.get(\"algorithm\", \"MD5\")\n            opaque = header_dict[\"opaque\"].encode() if \"opaque\" in header_dict else None\n            qop = header_dict[\"qop\"].encode() if \"qop\" in header_dict else None\n            return _DigestAuthChallenge(\n                realm=realm, nonce=nonce, algorithm=algorithm, opaque=opaque, qop=qop\n            )\n        except KeyError as exc:\n            message = \"Malformed Digest WWW-Authenticate header\"\n            raise ProtocolError(message, request=request) from exc\n\n    def _build_auth_header(\n        self, request: Request, challenge: _DigestAuthChallenge\n    ) -> str:\n        hash_func = self._ALGORITHM_TO_HASH_FUNCTION[challenge.algorithm.upper()]\n\n        def digest(data: bytes) -> bytes:\n            return hash_func(data).hexdigest().encode()\n\n        A1 = b\":\".join((self._username, challenge.realm, self._password))\n\n        path = request.url.raw_path\n        A2 = b\":\".join((request.method.encode(), path))\n        # TODO: implement auth-int\n        HA2 = digest(A2)\n\n        nc_value = b\"%08x\" % self._nonce_count\n        cnonce = self._get_client_nonce(self._nonce_count, challenge.nonce)","sourceCodeStart":235,"sourceCodeEnd":271,"githubUrl":"https://github.com/encode/httpx/blob/b5addb64f0161ff6bfe94c124ef76f6a1fba5254/httpx/_auth.py#L235-L271","documentation":"Raised as httpx.ProtocolError when DigestAuth._parse_challenge cannot find a required field. After a 401 with a 'WWW-Authenticate: Digest ...' header, httpx splits the header into key=value pairs and reads 'realm' and 'nonce' (KeyError on either triggers this). It indicates the server sent a Digest challenge missing RFC 7616 mandatory fields.","triggerScenarios":"Calling client.get(url, auth=httpx.DigestAuth(user, pw)) against a server whose 401 'WWW-Authenticate: Digest' header omits 'realm' or 'nonce' (e.g. a header like 'Digest opaque=\"x\"' with no realm/nonce).","commonSituations":"Non-compliant or custom auth servers/proxies that issue a Digest challenge without all mandatory fields; servers returning a 'stale' indicator without realm; misconfigured reverse proxies.","solutions":["Inspect the raw 401 response's WWW-Authenticate header (send once with follow_redirects=False / without auth) to see which fields are missing.","Do not use httpx.DigestAuth against that endpoint; switch to BasicAuth, a bearer token, or a custom Auth subclass.","Fix or reconfigure the server/proxy so its Digest challenge includes at least realm and nonce."],"exampleFix":"// before\nclient.get(url, auth=httpx.DigestAuth(\"u\", \"p\"))\n// after\nclient.get(url, headers={\"Authorization\": \"Bearer <token>\"})","handlingStrategy":"try-catch","validationCode":"import httpx\n# Probe the endpoint once to inspect the challenge before relying on DigestAuth\nprobe = client.get(url)\nif probe.status_code == 401:\n    wa = probe.headers.get_list(\"www-authenticate\")\n    has_digest = any(h.lower().startswith(\"digest \") for h in wa)\n    field_keys = set()\n    for h in wa:\n        if h.lower().startswith(\"digest \"):\n            for part in h.split(None, 1)[1].split(\",\"):\n                k = part.split(\"=\", 1)[0].strip()\n                field_keys.add(k.lower())\n    digest_ok = has_digest and {\"realm\", \"nonce\"} <= field_keys\n    # if not digest_ok, do not attempt DigestAuth","typeGuard":"def has_required_digest_fields(www_auth_values: list[str]) -> bool:\n    for h in www_auth_values:\n        if h.lower().startswith(\"digest \"):\n            fields = {}\n            for part in h.split(None, 1)[1].split(\",\"):\n                k, _, v = part.partition(\"=\")\n                fields[k.strip().lower()] = v.strip().strip('\"')\n            return \"realm\" in fields and \"nonce\" in fields\n    return False","tryCatchPattern":"try:\n    resp = client.get(url, auth=httpx.DigestAuth(user, pw))\nexcept httpx.ProtocolError as exc:\n    # Server returned a malformed Digest challenge\n    log.warning(\"Digest challenge malformed: %s\", exc)\n    resp = client.get(url, headers={\"Authorization\": \"Bearer <token>\"})","preventionTips":["Inspect the 401 WWW-Authenticate header before committing to DigestAuth.","Prefer token-based auth for non-RFC-compliant servers.","Always catch httpx.ProtocolError around authenticated requests."],"tags":["digest-auth","authentication","protocol"],"analyzedSha":"b5addb64f0161ff6bfe94c124ef76f6a1fba5254","analyzedAt":"2026-08-04T19:32:56.768Z","schemaVersion":2}