PrefectHQ/fastmcp · error · AuthorizeError

invalid_client

invalid_client

Error message

invalid_client: Client ID is required

What it means

Just before building the AuthorizationCode, authorize() asserts that the client has a non-None client_id, since the code must record which client it was issued to. A Client whose client_id is None triggers AuthorizeError('invalid_client', 'Client ID is required').

Source

Thrown at fastmcp_slim/fastmcp/server/auth/providers/in_memory.py:135

                # or if params.redirect_uri is None and client has a default.
                # However, the AuthorizationHandler handles the primary validation.
                pass  # Let's assume AuthorizationHandler did its job.
        except Exception as e:  # Replace with specific validation error if client.validate_redirect_uri existed
            raise AuthorizeError(
                error="invalid_request", error_description="Invalid redirect_uri."
            ) from e

        auth_code_value = f"test_auth_code_{secrets.token_hex(16)}"
        expires_at = time.time() + DEFAULT_AUTH_CODE_EXPIRY_SECONDS

        # Ensure scopes are a list
        scopes_list = params.scopes if params.scopes is not None else []
        if client.scope:  # Filter params.scopes against client's registered scopes
            client_allowed_scopes = set(client.scope.split())
            scopes_list = [s for s in scopes_list if s in client_allowed_scopes]

        if client.client_id is None:
            raise AuthorizeError(
                error="invalid_client", error_description="Client ID is required"
            )
        auth_code = AuthorizationCode(
            code=auth_code_value,
            client_id=client.client_id,
            redirect_uri=params.redirect_uri,
            redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,
            scopes=scopes_list,
            expires_at=expires_at,
            code_challenge=params.code_challenge,
            # code_challenge_method is assumed S256 by the framework
        )
        self.auth_codes[auth_code_value] = auth_code

        return construct_redirect_uri(
            str(params.redirect_uri), code=auth_code_value, state=params.state
        )

View on GitHub (pinned to 1f02114297)

Solutions

  1. Set client_id on the Client before calling authorize
  2. Verify registration flow so the Client passed downstream carries the registered id
  3. Add an assertion or guard in your own code: `assert client.client_id is not None` before authorization calls

Example fix

// before
client = Client(redirect_uris=[...])  # client_id defaults to None
provider.authorize(client, params)
// after
client = Client(client_id="app-1", redirect_uris=[...])
provider.authorize(client, params)
Defensive patterns

Strategy: type-guard

Validate before calling

if client.client_id is None:
    raise ValueError("Client must have client_id before authorize()")
provider.authorize(client, params)

Type guard

def has_client_id(client) -> bool:
    return isinstance(client.client_id, str) and bool(client.client_id)

Try / catch

try:
    redirect = provider.authorize(client, params)
except AuthorizeError as e:
    if e.error == "invalid_client":
        logger.error("Client object missing client_id: %s", client)

Prevention

When it happens

Trigger: Calling authorize() with a Client object constructed without a client_id (client_id=None) — the earlier `client.client_id not in self.clients` check passes only if None is somehow a key, so this typically follows partially-populated client objects in tests or handlers.

Common situations: Test fixtures building Client dataclasses with defaults; a token/authorization handler losing the id when mapping between metadata and Client; deserialization dropping an absent client_id field.

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/5a2729a1934f7817. Report an issue: GitHub.