mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 17:51:22 +00:00
refactor: dedupe agent error formatting
This commit is contained in:
@@ -27,6 +27,7 @@ import {
|
||||
emitAgentEvent,
|
||||
registerAgentRunContext,
|
||||
} from "../infra/agent-events.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { buildOutboundSessionContext } from "../infra/outbound/session-context.js";
|
||||
import { getRemoteSkillEligibility } from "../infra/skills-remote.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
@@ -480,7 +481,7 @@ async function agentCommandInternal(
|
||||
});
|
||||
} catch (error) {
|
||||
log.warn(
|
||||
`ACP transcript persistence failed for ${sessionKey}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
`ACP transcript persistence failed for ${sessionKey}: ${formatErrorMessage(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "@mariozechner/pi-ai/oauth";
|
||||
import { loadConfig, type OpenClawConfig } from "../../config/config.js";
|
||||
import { coerceSecretRef } from "../../config/types.secrets.js";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { withFileLock } from "../../infra/file-lock.js";
|
||||
import {
|
||||
formatProviderAuthProfileApiKeyWithPlugin,
|
||||
@@ -119,7 +120,7 @@ async function buildOAuthProfileResult(params: {
|
||||
}
|
||||
|
||||
function extractErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
return formatErrorMessage(error);
|
||||
}
|
||||
|
||||
function isRefreshTokenReusedError(error: unknown): boolean {
|
||||
@@ -207,7 +208,7 @@ function adoptNewerMainOAuthCredential(params: {
|
||||
// Best-effort: don't crash if main agent store is missing or unreadable.
|
||||
log.debug("adoptNewerMainOAuthCredential failed", {
|
||||
profileId: params.profileId,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: formatErrorMessage(err),
|
||||
});
|
||||
}
|
||||
return null;
|
||||
@@ -403,7 +404,7 @@ async function resolveProfileSecretString(params: {
|
||||
log.debug(params.inlineFailureMessage, {
|
||||
profileId: params.profileId,
|
||||
provider: params.provider,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: formatErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -421,7 +422,7 @@ async function resolveProfileSecretString(params: {
|
||||
log.debug(params.refFailureMessage, {
|
||||
profileId: params.profileId,
|
||||
provider: params.provider,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: formatErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { execFileSync, execSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { loadJsonFile, saveJsonFile } from "../infra/json-file.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
@@ -461,7 +462,7 @@ export function writeClaudeCliKeychainCredentials(
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.warn("failed to write credentials to claude cli keychain", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
error: formatErrorMessage(error),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
@@ -503,7 +504,7 @@ export function writeClaudeCliFileCredentials(
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.warn("failed to write credentials to claude cli file", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
error: formatErrorMessage(error),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
@@ -584,7 +585,7 @@ export function writeCodexCliKeychainCredentials(
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.warn("failed to write credentials to codex cli keychain", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
error: formatErrorMessage(error),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
@@ -614,7 +615,7 @@ export function writeCodexCliFileCredentials(
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.warn("failed to write credentials to codex cli file", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
error: formatErrorMessage(error),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ImageContent } from "@mariozechner/pi-ai";
|
||||
import type { ThinkLevel } from "../auto-reply/thinking.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { executePreparedCliRun } from "./cli-runner/execute.js";
|
||||
import { prepareCliRunContext } from "./cli-runner/prepare.js";
|
||||
import type { RunCliAgentParams } from "./cli-runner/types.js";
|
||||
@@ -73,7 +74,7 @@ export async function runCliAgent(params: RunCliAgentParams): Promise<EmbeddedPi
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const message = formatErrorMessage(err);
|
||||
if (isFailoverErrorMessage(message, { provider: params.provider })) {
|
||||
const reason = classifyFailoverReason(message, { provider: params.provider }) ?? "unknown";
|
||||
const status = resolveFailoverStatus(reason);
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
resolveAgentModelFallbackValues,
|
||||
resolveAgentModelPrimaryValue,
|
||||
} from "../config/model-input.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { sanitizeForLog } from "../terminal/ansi.js";
|
||||
import { resolveAuthProfileOrder } from "./auth-profiles/order.js";
|
||||
@@ -814,7 +815,7 @@ export async function runWithModelFallback<T>(params: {
|
||||
// compaction/retry logic, not by model fallback. If one escapes as a
|
||||
// throw, rethrow it immediately rather than trying a different model
|
||||
// that may have a smaller context window and fail worse.
|
||||
const errMessage = err instanceof Error ? err.message : String(err);
|
||||
const errMessage = formatErrorMessage(err);
|
||||
if (isLikelyContextOverflowError(errMessage)) {
|
||||
throw err;
|
||||
}
|
||||
@@ -937,7 +938,7 @@ export async function runWithImageModelFallback<T>(params: {
|
||||
attempts.push({
|
||||
provider: candidate.provider,
|
||||
model: candidate.model,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: formatErrorMessage(err),
|
||||
});
|
||||
await params.onError?.({
|
||||
provider: candidate.provider,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type Tool,
|
||||
} from "@mariozechner/pi-ai";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { inferParamBFromIdOrName } from "../shared/model-param-b.js";
|
||||
import { normalizeProviderId } from "./provider-id.js";
|
||||
|
||||
@@ -284,7 +285,7 @@ async function probeTool(
|
||||
return {
|
||||
ok: false,
|
||||
latencyMs: Date.now() - startedAt,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: formatErrorMessage(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -321,7 +322,7 @@ async function probeImage(
|
||||
return {
|
||||
ok: false,
|
||||
latencyMs: Date.now() - startedAt,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: formatErrorMessage(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import {
|
||||
groupPluginDiscoveryProvidersByOrder,
|
||||
@@ -322,7 +323,7 @@ async function runProviderCatalogWithTimeout(
|
||||
}),
|
||||
]);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const message = formatErrorMessage(error);
|
||||
if (message.includes("provider catalog timed out after")) {
|
||||
log.warn(`${message}; skipping provider discovery`);
|
||||
return undefined;
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||
import type {
|
||||
AssistantMessage,
|
||||
AssistantMessageEvent,
|
||||
AssistantMessageEventStream,
|
||||
StopReason,
|
||||
} from "@mariozechner/pi-ai";
|
||||
import * as piAi from "@mariozechner/pi-ai";
|
||||
/**
|
||||
* OpenAI WebSocket StreamFn Integration
|
||||
*
|
||||
@@ -20,16 +29,7 @@
|
||||
*
|
||||
* @see src/agents/openai-ws-connection.ts for the connection manager
|
||||
*/
|
||||
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||
import type {
|
||||
AssistantMessage,
|
||||
AssistantMessageEvent,
|
||||
AssistantMessageEventStream,
|
||||
StopReason,
|
||||
} from "@mariozechner/pi-ai";
|
||||
import * as piAi from "@mariozechner/pi-ai";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import {
|
||||
resolveProviderTransportTurnStateWithPlugin,
|
||||
resolveProviderWebSocketSessionPolicyWithPlugin,
|
||||
@@ -550,7 +550,7 @@ function normalizeWsRunError(err: unknown): OpenAIWebSocketRuntimeError {
|
||||
if (err instanceof OpenAIWebSocketRuntimeError) {
|
||||
return err;
|
||||
}
|
||||
return new OpenAIWebSocketRuntimeError(err instanceof Error ? err.message : String(err), {
|
||||
return new OpenAIWebSocketRuntimeError(formatErrorMessage(err), {
|
||||
kind: "server",
|
||||
retryable: false,
|
||||
});
|
||||
@@ -1177,7 +1177,7 @@ export function createOpenAIWebSocketStreamFn(
|
||||
|
||||
queueMicrotask(() =>
|
||||
run().catch((err) => {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
const errorMessage = formatErrorMessage(err);
|
||||
log.warn(`[ws-stream] session=${sessionId} run error: ${errorMessage}`);
|
||||
eventStream.push({
|
||||
type: "error",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { extractAssistantText, stripToolMessages } from "./tools/chat-history-text.js";
|
||||
|
||||
type GatewayCaller = typeof callGateway;
|
||||
@@ -137,7 +138,7 @@ export async function waitForAgentRun(params: {
|
||||
}
|
||||
return normalizeAgentWaitResult("ok", wait);
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err.message : String(err);
|
||||
const error = formatErrorMessage(err);
|
||||
return {
|
||||
status: error.includes("gateway timeout") ? "timeout" : "error",
|
||||
error,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { complete, type Api, type Model } from "@mariozechner/pi-ai";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { resolveAgentDir, resolveAgentEffectiveModelPrimary } from "./agent-scope.js";
|
||||
import { DEFAULT_PROVIDER } from "./defaults.js";
|
||||
import {
|
||||
@@ -152,7 +153,7 @@ export async function prepareSimpleCompletionModel(params: {
|
||||
});
|
||||
} catch (err) {
|
||||
return {
|
||||
error: `Auth lookup failed for provider "${resolved.model.provider}": ${err instanceof Error ? err.message : String(err)}`,
|
||||
error: `Auth lookup failed for provider "${resolved.model.provider}": ${formatErrorMessage(err)}`,
|
||||
};
|
||||
}
|
||||
const rawApiKey = auth.apiKey?.trim();
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type ClawHubSkillDetail,
|
||||
type ClawHubSkillSearchResult,
|
||||
} from "../infra/clawhub.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { withExtractedArchiveRoot } from "../infra/install-flow.js";
|
||||
import { installPackageDir } from "../infra/install-package-dir.js";
|
||||
import { resolveSafeInstallDir } from "../infra/install-safe-path.js";
|
||||
@@ -336,7 +337,7 @@ async function performClawHubSkillInstall(
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: formatErrorMessage(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -352,7 +353,7 @@ async function installRequestedSkillFromClawHub(
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: formatErrorMessage(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -368,7 +369,7 @@ async function installTrackedSkillFromClawHub(
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: formatErrorMessage(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Readable } from "node:stream";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import type { ReadableStream as NodeReadableStream } from "node:stream/web";
|
||||
import { isWindowsDrivePath } from "../infra/archive-path.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { writeFileFromPathWithinRoot } from "../infra/fs-safe.js";
|
||||
import { assertCanonicalPathWithinBase } from "../infra/install-safe-path.js";
|
||||
import { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js";
|
||||
@@ -151,7 +152,7 @@ export async function installDownloadSpec(params: {
|
||||
const targetRelativePath = path.relative(safeRoot, requestedTargetDir);
|
||||
targetDir = path.join(canonicalSafeRoot, targetRelativePath);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const message = formatErrorMessage(err);
|
||||
return { ok: false, message, stdout: "", stderr: message, code: null };
|
||||
}
|
||||
|
||||
@@ -181,7 +182,7 @@ export async function installDownloadSpec(params: {
|
||||
});
|
||||
downloaded = result.bytes;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const message = formatErrorMessage(err);
|
||||
return { ok: false, message, stdout: "", stderr: message, code: null };
|
||||
}
|
||||
|
||||
@@ -214,7 +215,7 @@ export async function installDownloadSpec(params: {
|
||||
boundaryLabel: "skill tools directory",
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const message = formatErrorMessage(err);
|
||||
return { ok: false, message, stdout: "", stderr: message, code: null };
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
prepareArchiveDestinationDir,
|
||||
withStagedArchiveDestination,
|
||||
} from "../infra/archive.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { parseTarVerboseMetadata } from "./skills-install-tar-verbose.js";
|
||||
import { hasBinary } from "./skills.js";
|
||||
@@ -227,7 +228,7 @@ export async function extractArchive(params: {
|
||||
|
||||
return { stdout: "", stderr: `unsupported archive type: ${archiveType}`, code: null };
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const message = formatErrorMessage(err);
|
||||
return { stdout: "", stderr: message, code: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveBrewExecutable } from "../infra/brew.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import {
|
||||
type InstallSafetyOverrides,
|
||||
scanSkillInstallSource,
|
||||
@@ -248,7 +249,7 @@ async function runCommandSafely(
|
||||
return {
|
||||
code: null,
|
||||
stdout: "",
|
||||
stderr: err instanceof Error ? err.message : String(err),
|
||||
stderr: formatErrorMessage(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { SessionEntry } from "../config/sessions.js";
|
||||
import { loadSessionStore, resolveStorePath, updateSessionStore } from "../config/sessions.js";
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
import { logVerbose } from "../globals.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { isSubagentSessionKey, parseAgentSessionKey } from "../routing/session-key.js";
|
||||
import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
|
||||
import { AGENT_LANE_SUBAGENT } from "./lanes.js";
|
||||
@@ -171,9 +172,7 @@ async function killSubagentRun(params: {
|
||||
});
|
||||
} catch (error) {
|
||||
logVerbose(
|
||||
`subagents control kill: failed to persist abortedLastRun for ${childSessionKey}: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
`subagents control kill: failed to persist abortedLastRun for ${childSessionKey}: ${formatErrorMessage(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -557,7 +556,7 @@ export async function steerControlledSubagentRun(params: {
|
||||
}
|
||||
} catch (err) {
|
||||
clearSubagentRunSteerRestart(params.entry.runId);
|
||||
const error = err instanceof Error ? err.message : String(err);
|
||||
const error = formatErrorMessage(err);
|
||||
return {
|
||||
status: "error",
|
||||
runId,
|
||||
@@ -681,7 +680,7 @@ export async function sendControlledSubagentMessage(params: {
|
||||
}
|
||||
return { status: "ok" as const, runId, replyText: result.replyText };
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err.message : String(err);
|
||||
const error = formatErrorMessage(err);
|
||||
return { status: "error" as const, runId, error };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
resolveLeastPrivilegeOperatorScopesForMethod,
|
||||
type OperatorScope,
|
||||
} from "../../gateway/method-scopes.js";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
|
||||
import { readStringParam } from "./common.js";
|
||||
|
||||
@@ -32,7 +33,7 @@ function canonicalizeToolGatewayWsUrl(raw: string): { origin: string; key: strin
|
||||
try {
|
||||
url = new URL(input);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const message = formatErrorMessage(error);
|
||||
throw new Error(`invalid gatewayUrl: ${input} (${message})`, { cause: error });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import crypto from "node:crypto";
|
||||
import { parseTimeoutMs } from "../../cli/parse-timeout.js";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { jsonResult, readStringParam } from "./common.js";
|
||||
import type { GatewayCallOptions } from "./gateway.js";
|
||||
import { callGatewayTool } from "./gateway.js";
|
||||
@@ -138,7 +139,7 @@ export async function executeNodeCommandAction(params: {
|
||||
try {
|
||||
invokeParams = JSON.parse(invokeParamsJson);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const message = formatErrorMessage(err);
|
||||
throw new Error(`invokeParamsJson must be valid JSON: ${message}`, {
|
||||
cause: err,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { isAcpSessionKey, normalizeMainKey } from "../../routing/session-key.js";
|
||||
import { looksLikeSessionId } from "../../sessions/session-id.js";
|
||||
|
||||
@@ -274,7 +275,7 @@ async function resolveSessionKeyFromSessionId(params: {
|
||||
error: `Session not visible from this sandboxed agent session: ${params.sessionId}`,
|
||||
};
|
||||
}
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const message = formatErrorMessage(err);
|
||||
return {
|
||||
ok: false,
|
||||
status: "error",
|
||||
|
||||
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { normalizeAgentId, resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
|
||||
import { SESSION_LABEL_MAX_LENGTH } from "../../sessions/session-label.js";
|
||||
import {
|
||||
@@ -156,7 +157,7 @@ export function createSessionsSendTool(opts?: {
|
||||
});
|
||||
resolvedKey = typeof resolved?.key === "string" ? resolved.key.trim() : "";
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
const msg = formatErrorMessage(err);
|
||||
if (restrictToSpawned) {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
|
||||
Reference in New Issue
Block a user