{"record":{"id":"b633a88aac98ced3","repo":"siyuan-note/siyuan","slug":"oauth-revocation-endpoint-returned-s","errorCode":null,"errorMessage":"OAuth revocation endpoint returned %s","messagePattern":"OAuth revocation endpoint returned (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/mcp/client/oauth.go","lineNumber":768,"sourceCode":"\t\t}\n\t\tvalues := url.Values{\"token\": {token.value}, \"token_type_hint\": {token.hint}}\n\t\tapplyOAuthClientAuthentication(values, nil, credential)\n\t\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, credential.RevocationEndpoint, strings.NewReader(values.Encode()))\n\t\tif err != nil {\n\t\t\tresult = errors.Join(result, err)\n\t\t\tcontinue\n\t\t}\n\t\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t\tapplyOAuthClientAuthentication(nil, req, credential)\n\t\tresp, err := client.Do(req)\n\t\tif err != nil {\n\t\t\tresult = errors.Join(result, err)\n\t\t\tcontinue\n\t\t}\n\t\tio.Copy(io.Discard, io.LimitReader(resp.Body, 1<<20))\n\t\tresp.Body.Close()\n\t\tif resp.StatusCode < 200 || resp.StatusCode >= 300 {\n\t\t\tresult = errors.Join(result, fmt.Errorf(\"OAuth revocation endpoint returned %s\", resp.Status))\n\t\t}\n\t}\n\tif result != nil {\n\t\tlogging.LogWarnf(\"mcp oauth: revoke credentials failed: %s\", result)\n\t}\n\treturn result\n}\n\nfunc isSecureOAuthEndpoint(endpoint string) bool {\n\tparsed, err := url.Parse(endpoint)\n\tif err != nil {\n\t\treturn false\n\t}\n\tif parsed.Scheme == \"https\" {\n\t\treturn true\n\t}\n\tif parsed.Scheme != \"http\" {\n\t\treturn false","sourceCodeStart":750,"sourceCodeEnd":786,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/mcp/client/oauth.go#L750-L786","documentation":"The OAuth revocation endpoint returned a non-2xx HTTP status for at least one of the tokens being revoked. Revocation runs in a best-effort goroutine during `DisconnectMCPOAuth`; the local credential is already removed, so this error only affects whether the IdP also invalidates the token server-side.","triggerScenarios":"`revokeOAuthCredential` POSTs the access and/or refresh token to `RevocationEndpoint` and receives `statusCode < 200 || >= 300`; the error is joined into `result` via `errors.Join`.","commonSituations":"The token was already expired or revoked by the IdP. The `client_secret_basic`/`client_secret_post` auth method at the revocation endpoint differs from the token endpoint. Transient IdP outage during disconnect.","solutions":["Check the embedded `resp.Status` for the exact reason (e.g. `401 Unauthorized` → wrong client auth).","Verify the client authentication method and credentials are accepted at the revocation endpoint, not just the token endpoint.","Since local state is already cleared, this is non-fatal; the token will simply expire naturally at the IdP if revocation keeps failing."],"exampleFix":null,"handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Revocation is best-effort: log and continue; local credentials are already gone.\nif err := revokeOAuthCredential(ctx, client, cred); err != nil {\n    logging.LogWarnf(\"revoke failed (non-fatal): %s\", err)\n}","preventionTips":["Treat revocation failure as non-fatal; local state is already cleared.","Verify the revocation endpoint accepts the same client auth method as the token endpoint.","Let tokens expire naturally at the IdP if revocation keeps failing."],"tags":["oauth","mcp","network","http","best-effort"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}