aio-libs/aiohttp · error · ClientError

Malformed Digest auth challenge: Missing 'nonce' parameter

Error message

Malformed Digest auth challenge: Missing 'nonce' parameter

What it means

Raised by aiohttp's Digest auth middleware when the server's Digest challenge is missing the 'nonce' parameter. The nonce is a server-issued, single-use value essential to Digest auth's replay protection; without it the digest response cannot be computed. aiohttp raises ClientError from _encode() rather than silently sending an unauthenticated or broken request.

Solutions

  1. Capture the raw WWW-Authenticate header with a plain request to confirm nonce is absent.
  2. Correct the server/proxy to include nonce in its Digest challenge (RFC 7616 requires it).
  3. If the server is third-party and unfixable, remove DigestAuthMiddleware and negotiate a supported scheme (Basic, Bearer).
  4. Check that the challenge is actually Digest and not a Basic challenge being misrouted to the Digest middleware.

Example fix

# before
mw = DigestAuthMiddleware(login='u', password='p')
await session.get('https://srv/protected')  # challenge = 'Digest realm="x"' (no nonce)

# after — verify and fix server header to include nonce
# expected: WWW-Authenticate: Digest realm="x", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093"
Defensive patterns

Strategy: try-catch

Validate before calling

async def challenge_has_nonce(session, url) -> bool:
    resp = await session.get(url)
    wa = resp.headers.get('WWW-Authenticate', '')
    await resp.release()
    return 'nonce=' in wa.lower()

Type guard

def challenge_valid(challenge_header: str) -> bool:
    low = challenge_header.lower()
    return low.startswith('digest') and 'realm=' in low and 'nonce=' in low

Try / catch

from aiohttp import ClientError

try:
    resp = await session.get(url)
except ClientError as e:
    if "Missing 'nonce'" in str(e):
        # server challenge is incomplete; cannot authenticate
        handle_bad_challenge(e)
    raise

Prevention

When it happens

Trigger: Sending a request through DigestAuthMiddleware against a server whose 'WWW-Authenticate: Digest' header contains a realm but no nonce (e.g. 'WWW-Authenticate: Digest realm="x"'). Fires during the challenge-encoding step after the 401 response triggers a re-authentication attempt.

Common situations: Custom or buggy server-side Digest implementations that emit an incomplete challenge; middleware/CDN that rewrites the challenge and drops nonce; testing against a mock server that hardcodes a partial header; protocol confusion (server sends Basic but client expects Digest).

Understand the failure class

Related errors


AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11). Data as JSON: /api/errors/511f23d191ead1fd. Report an issue: GitHub.

Appendix: source

Thrown at aiohttp/client_middleware_digest_auth.py:248

            url: The request URL
            body: The request body (used for qop=auth-int)

        Returns:
            A fully formatted Digest authorization header string

        Raises:
            ClientError: If the challenge is missing required parameters or
                         contains unsupported values

        """
        challenge = self._challenge
        if "realm" not in challenge:
            raise ClientError(
                "Malformed Digest auth challenge: Missing 'realm' parameter"
            )

        if "nonce" not in challenge:
            raise ClientError(
                "Malformed Digest auth challenge: Missing 'nonce' parameter"
            )

        # Empty realm values are allowed per RFC 7616 (SHOULD, not MUST, contain host name)
        realm = challenge["realm"]
        nonce = challenge["nonce"]

        # Empty nonce values are not allowed as they are security-critical for replay protection
        if not nonce:
            raise ClientError(
                "Security issue: Digest auth challenge contains empty 'nonce' value"
            )

        qop_raw = challenge.get("qop", "")
        # Preserve original algorithm case for response while using uppercase for processing
        algorithm_original = challenge.get("algorithm", "MD5")
        algorithm = algorithm_original.upper()
        opaque = challenge.get("opaque", "")

View on GitHub (pinned to d041d4d0fd)