Merge branch 'litellm_yj_may7' into cve-sweep-2026-05
This commit is contained in:
commit
309cc36f9d
@ -68,8 +68,8 @@ FROM $LITELLM_RUNTIME_IMAGE AS runtime
|
||||
|
||||
USER root
|
||||
|
||||
RUN apk add --no-cache bash openssl tzdata nodejs npm python3 libsndfile supervisor && \
|
||||
npm install -g npm@11.14.0 tar@7.5.11 glob@13.0.6 @isaacs/brace-expansion@5.0.1 brace-expansion@5.0.5 minimatch@10.2.4 diff@8.0.3 picomatch@4.0.4 && \
|
||||
RUN apk add --no-cache bash openssl tzdata nodejs npm python3 libsndfile && \
|
||||
npm install -g npm@11.12.1 tar@7.5.11 glob@13.0.6 @isaacs/brace-expansion@5.0.1 brace-expansion@5.0.5 minimatch@10.2.4 diff@8.0.3 picomatch@4.0.4 && \
|
||||
GLOBAL="$(npm root -g)" && \
|
||||
for pkg in tar glob @isaacs/brace-expansion brace-expansion minimatch diff picomatch; do \
|
||||
name="${pkg##*/}"; \
|
||||
@ -97,7 +97,5 @@ RUN find /app/.venv -type f -path "*/tornado/test/*" -delete && \
|
||||
|
||||
EXPOSE 4000/tcp
|
||||
|
||||
COPY docker/supervisord.conf /etc/supervisord.conf
|
||||
|
||||
ENTRYPOINT ["docker/prod_entrypoint.sh"]
|
||||
CMD ["--port", "4000"]
|
||||
|
||||
@ -129,12 +129,6 @@ spec:
|
||||
value: {{ $val | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.separateHealthApp }}
|
||||
- name: SEPARATE_HEALTH_APP
|
||||
value: "1"
|
||||
- name: SEPARATE_HEALTH_PORT
|
||||
value: {{ .Values.separateHealthPort | default "8081" | quote }}
|
||||
{{- end }}
|
||||
{{- with .Values.extraEnvVars }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
@ -175,15 +169,10 @@ spec:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.port }}
|
||||
protocol: TCP
|
||||
{{- if .Values.separateHealthApp }}
|
||||
- name: health
|
||||
containerPort: {{ .Values.separateHealthPort | default 8081 }}
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.livenessProbe.path | quote }}
|
||||
port: {{ if .Values.separateHealthApp }}"health"{{ else }}"http"{{ end }}
|
||||
port: "http"
|
||||
initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }}
|
||||
@ -192,7 +181,7 @@ spec:
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.readinessProbe.path | quote }}
|
||||
port: {{ if .Values.separateHealthApp }}"health"{{ else }}"http"{{ end }}
|
||||
port: "http"
|
||||
initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }}
|
||||
@ -201,7 +190,7 @@ spec:
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.startupProbe.path | quote }}
|
||||
port: {{ if .Values.separateHealthApp }}"health"{{ else }}"http"{{ end }}
|
||||
port: "http"
|
||||
initialDelaySeconds: {{ .Values.startupProbe.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.startupProbe.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.startupProbe.timeoutSeconds }}
|
||||
|
||||
@ -88,12 +88,6 @@ service:
|
||||
# optionally specify loadBalancerClass
|
||||
# loadBalancerClass: tailscale
|
||||
|
||||
# Separate health app configuration
|
||||
# When enabled, health checks will use a separate port and the application
|
||||
# will receive SEPARATE_HEALTH_APP=1 and SEPARATE_HEALTH_PORT from environment variables
|
||||
separateHealthApp: false
|
||||
separateHealthPort: 8081
|
||||
|
||||
# Probes for LiteLLM gateway container
|
||||
livenessProbe:
|
||||
path: /health/liveliness
|
||||
|
||||
@ -66,7 +66,7 @@ FROM $LITELLM_RUNTIME_IMAGE AS runtime
|
||||
|
||||
USER root
|
||||
|
||||
RUN apk add --no-cache bash openssl tzdata nodejs npm python3 libsndfile supervisor && \
|
||||
RUN apk add --no-cache bash openssl tzdata nodejs npm python3 libsndfile && \
|
||||
npm install -g npm@11.12.1 tar@7.5.11 glob@11.1.0 @isaacs/brace-expansion@5.0.1 minimatch@10.2.4 diff@8.0.3 && \
|
||||
GLOBAL="$(npm root -g)" && \
|
||||
find "$GLOBAL/npm" -type d -name "tar" -path "*/node_modules/tar" | while read d; do \
|
||||
@ -102,7 +102,5 @@ RUN find /app/.venv -type f -path "*/tornado/test/*" -delete && \
|
||||
|
||||
EXPOSE 4000/tcp
|
||||
|
||||
COPY docker/supervisord.conf /etc/supervisord.conf
|
||||
|
||||
ENTRYPOINT ["docker/prod_entrypoint.sh"]
|
||||
CMD ["--port", "4000"]
|
||||
|
||||
@ -103,13 +103,12 @@ RUN for i in 1 2 3; do \
|
||||
apk upgrade --no-cache && break || sleep 5; \
|
||||
done && \
|
||||
for i in 1 2 3; do \
|
||||
apk add --no-cache python3 bash openssl tzdata supervisor libsndfile nodejs && break || sleep 5; \
|
||||
apk add --no-cache python3 bash openssl tzdata libsndfile nodejs && break || sleep 5; \
|
||||
done
|
||||
|
||||
COPY --from=builder /app /app
|
||||
COPY --from=builder /var/lib/litellm/ui /var/lib/litellm/ui
|
||||
COPY --from=builder /var/lib/litellm/assets /var/lib/litellm/assets
|
||||
COPY --from=builder /app/docker/supervisord.conf /etc/supervisord.conf
|
||||
|
||||
ENV PATH="/app/.venv/bin:${PATH}" \
|
||||
PRISMA_BINARY_CACHE_DIR=/app/.cache/prisma-python/binaries \
|
||||
|
||||
@ -1,14 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ "$SEPARATE_HEALTH_APP" = "1" ]; then
|
||||
export LITELLM_ARGS="$@"
|
||||
export SUPERVISORD_STOPWAITSECS="${SUPERVISORD_STOPWAITSECS:-3600}"
|
||||
exec supervisord -c /etc/supervisord.conf
|
||||
fi
|
||||
|
||||
if [ "$USE_DDTRACE" = "true" ]; then
|
||||
export DD_TRACE_OPENAI_ENABLED="False"
|
||||
exec ddtrace-run litellm "$@"
|
||||
else
|
||||
exec litellm "$@"
|
||||
fi
|
||||
fi
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
loglevel=info
|
||||
logfile=/tmp/supervisord.log
|
||||
pidfile=/tmp/supervisord.pid
|
||||
|
||||
[group:litellm]
|
||||
programs=main,health
|
||||
|
||||
[program:main]
|
||||
command=sh -c 'if [ "$USE_DDTRACE" = "true" ]; then export DD_TRACE_OPENAI_ENABLED="False"; exec ddtrace-run python -m litellm.proxy.proxy_cli --host 0.0.0.0 --port=4000 $LITELLM_ARGS; else exec python -m litellm.proxy.proxy_cli --host 0.0.0.0 --port=4000 $LITELLM_ARGS; fi'
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startretries=3
|
||||
priority=1
|
||||
exitcodes=0
|
||||
stopasgroup=true
|
||||
killasgroup=true
|
||||
stopwaitsecs=%(ENV_SUPERVISORD_STOPWAITSECS)s
|
||||
stdout_logfile=/dev/stdout
|
||||
stderr_logfile=/dev/stderr
|
||||
stdout_logfile_maxbytes = 0
|
||||
stderr_logfile_maxbytes = 0
|
||||
environment=PYTHONUNBUFFERED=true
|
||||
|
||||
[program:health]
|
||||
command=sh -c '[ "$SEPARATE_HEALTH_APP" = "1" ] && exec uvicorn litellm.proxy.health_endpoints.health_app_factory:build_health_app --factory --host 0.0.0.0 --port=${SEPARATE_HEALTH_PORT:-4001} || exit 0'
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startretries=3
|
||||
priority=2
|
||||
exitcodes=0
|
||||
stopasgroup=true
|
||||
killasgroup=true
|
||||
stopwaitsecs=%(ENV_SUPERVISORD_STOPWAITSECS)s
|
||||
stdout_logfile=/dev/stdout
|
||||
stderr_logfile=/dev/stderr
|
||||
stdout_logfile_maxbytes = 0
|
||||
stderr_logfile_maxbytes = 0
|
||||
environment=PYTHONUNBUFFERED=true
|
||||
|
||||
[eventlistener:process_monitor]
|
||||
command=python -c "from supervisor import childutils; import os, signal; [os.kill(os.getppid(), signal.SIGTERM) for h,p in iter(lambda: childutils.listener.wait(), None) if h['eventname'] in ['PROCESS_STATE_FATAL', 'PROCESS_STATE_EXITED'] and dict([x.split(':') for x in p.split(' ')])['processname'] in ['main', 'health'] or childutils.listener.ok()]"
|
||||
events=PROCESS_STATE_EXITED,PROCESS_STATE_FATAL
|
||||
autostart=true
|
||||
autorestart=true
|
||||
@ -1,8 +0,0 @@
|
||||
from fastapi import FastAPI
|
||||
from litellm.proxy.health_endpoints._health_endpoints import router as health_router
|
||||
|
||||
|
||||
def build_health_app():
|
||||
health_app = FastAPI(title="LiteLLM Health Endpoints")
|
||||
health_app.include_router(health_router)
|
||||
return health_app
|
||||
@ -1050,11 +1050,6 @@ def run_server( # noqa: PLR0915
|
||||
litellm_settings=litellm_settings if config else None, # type: ignore[possibly-unbound]
|
||||
)
|
||||
|
||||
# --- SEPARATE HEALTH APP LOGIC ---
|
||||
# To run the health app separately, use:
|
||||
# uvicorn litellm.proxy.health_app_factory:build_health_app --factory --host 0.0.0.0 --port=4001
|
||||
# This is compatible with the SEPARATE_HEALTH_APP Docker/supervisord pattern.
|
||||
# --- END SEPARATE HEALTH APP LOGIC ---
|
||||
# Skip server startup if requested (after all setup is done)
|
||||
if skip_server_startup:
|
||||
print( # noqa
|
||||
|
||||
@ -10,18 +10,22 @@ authors = [
|
||||
{ name = "BerriAI" },
|
||||
]
|
||||
dependencies = [
|
||||
"fastuuid==0.14.0",
|
||||
"httpx==0.28.1",
|
||||
"openai==2.33.0",
|
||||
"python-dotenv==1.2.2",
|
||||
"tiktoken==0.12.0",
|
||||
"importlib-metadata==8.5.0",
|
||||
"tokenizers==0.23.1",
|
||||
"click==8.1.8",
|
||||
"jinja2==3.1.6",
|
||||
"aiohttp==3.13.4",
|
||||
"pydantic==2.12.5",
|
||||
"jsonschema==4.23.0",
|
||||
# Ranges (not exact pins) so SDK consumers can coexist with their other
|
||||
# deps. Reproducibility for our Docker/CI comes from `uv.lock`.
|
||||
# When changing a floor, verify it installs + imports on every supported
|
||||
# Python with: `uv pip install --resolution=lowest-direct .`
|
||||
"fastuuid>=0.14.0,<1.0",
|
||||
"httpx>=0.28.0,<1.0",
|
||||
"openai>=2.20.0,<3.0.0",
|
||||
"python-dotenv>=1.0.0,<2.0",
|
||||
"tiktoken>=0.8.0,<1.0",
|
||||
"importlib-metadata>=8.0.0,<9.0",
|
||||
"tokenizers>=0.21.0,<1.0",
|
||||
"click>=8.0.0,<9.0",
|
||||
"jinja2>=3.1.0,<4.0",
|
||||
"aiohttp>=3.10,<4.0",
|
||||
"pydantic>=2.10.0,<3.0.0",
|
||||
"jsonschema>=4.0.0,<5.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@ -2,7 +2,6 @@ import os
|
||||
import sys
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import fastapi
|
||||
import pytest
|
||||
|
||||
sys.path.insert(
|
||||
@ -12,7 +11,6 @@ sys.path.insert(
|
||||
import builtins
|
||||
import types
|
||||
|
||||
from litellm.proxy.health_endpoints.health_app_factory import build_health_app
|
||||
from litellm.proxy.proxy_cli import ProxyInitializationHelpers
|
||||
|
||||
|
||||
@ -771,62 +769,8 @@ class TestProxyInitializationHelpers:
|
||||
mock_uvicorn_run.assert_called_once()
|
||||
|
||||
|
||||
class TestHealthAppFactory:
|
||||
"""Test cases for the health app factory module"""
|
||||
|
||||
def test_build_health_app(self):
|
||||
"""Test that build_health_app creates a FastAPI app with the correct title and includes the health router"""
|
||||
# Execute
|
||||
health_app = build_health_app()
|
||||
|
||||
# Assert
|
||||
assert health_app.title == "LiteLLM Health Endpoints"
|
||||
assert isinstance(health_app, fastapi.FastAPI)
|
||||
|
||||
# Verify that the app has the expected health endpoints by checking route paths
|
||||
# When a router is included, its routes are flattened into the main app's routes
|
||||
route_paths = []
|
||||
for route in health_app.routes:
|
||||
if hasattr(route, "path"):
|
||||
route_paths.append(route.path)
|
||||
|
||||
# Check for some expected health endpoints
|
||||
expected_paths = [
|
||||
"/test",
|
||||
"/health/services",
|
||||
"/health",
|
||||
"/health/history",
|
||||
"/health/latest",
|
||||
"/settings",
|
||||
"/active/callbacks",
|
||||
"/health/readiness",
|
||||
"/health/liveliness",
|
||||
"/health/liveness",
|
||||
"/health/test_connection",
|
||||
]
|
||||
|
||||
# At least some of the expected health endpoints should be present
|
||||
found_paths = [path for path in expected_paths if path in route_paths]
|
||||
assert (
|
||||
len(found_paths) > 0
|
||||
), f"Expected to find health endpoints, but found: {route_paths}"
|
||||
|
||||
# Verify that the app has routes (indicating the router was included)
|
||||
assert (
|
||||
len(health_app.routes) > 0
|
||||
), "Health app should have routes from the included router"
|
||||
|
||||
def test_build_health_app_returns_different_instances(self):
|
||||
"""Test that build_health_app returns different FastAPI instances on each call"""
|
||||
# Execute
|
||||
health_app_1 = build_health_app()
|
||||
health_app_2 = build_health_app()
|
||||
|
||||
# Assert
|
||||
assert health_app_1 is not health_app_2
|
||||
assert health_app_1.title == health_app_2.title
|
||||
assert isinstance(health_app_1, fastapi.FastAPI)
|
||||
assert isinstance(health_app_2, fastapi.FastAPI)
|
||||
class TestRunServerDbSetup:
|
||||
"""Tests for run_server's prisma setup_database behavior."""
|
||||
|
||||
@patch("subprocess.run")
|
||||
@patch("atexit.register")
|
||||
|
||||
26
uv.lock
generated
26
uv.lock
generated
@ -9,7 +9,7 @@ resolution-markers = [
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2026-05-02T11:18:44.200141Z"
|
||||
exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
|
||||
exclude-newer-span = "P3D"
|
||||
|
||||
[manifest]
|
||||
@ -3374,7 +3374,7 @@ proxy-dev = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "a2a-sdk", marker = "extra == 'extra-proxy'", specifier = "==0.3.24" },
|
||||
{ name = "aiohttp", specifier = "==3.13.4" },
|
||||
{ name = "aiohttp", specifier = ">=3.10,<4.0" },
|
||||
{ name = "anthropic", extras = ["vertex"], marker = "extra == 'proxy-runtime'", specifier = "==0.84.0" },
|
||||
{ name = "apscheduler", marker = "extra == 'proxy'", specifier = "==3.11.2" },
|
||||
{ name = "audioread", marker = "extra == 'stt-nvidia-riva'", specifier = ">=3.0.1" },
|
||||
@ -3387,14 +3387,14 @@ requires-dist = [
|
||||
{ name = "azure-storage-file-datalake", marker = "extra == 'proxy-runtime'", specifier = "==12.20.0" },
|
||||
{ name = "backoff", marker = "extra == 'proxy'", specifier = "==2.2.1" },
|
||||
{ name = "boto3", marker = "extra == 'proxy'", specifier = "==1.43.1" },
|
||||
{ name = "click", specifier = "==8.1.8" },
|
||||
{ name = "click", specifier = ">=8.0.0,<9.0" },
|
||||
{ name = "cryptography", marker = "extra == 'proxy'", specifier = "==46.0.7" },
|
||||
{ name = "ddtrace", marker = "extra == 'proxy-runtime'", specifier = "==2.19.0" },
|
||||
{ name = "detect-secrets", marker = "extra == 'proxy-runtime'", specifier = "==1.5.0" },
|
||||
{ name = "diskcache", marker = "extra == 'caching'", specifier = "==5.6.3" },
|
||||
{ name = "fastapi", marker = "extra == 'proxy'", specifier = "==0.124.4" },
|
||||
{ name = "fastapi-sso", marker = "extra == 'proxy'", specifier = "==0.19.0" },
|
||||
{ name = "fastuuid", specifier = "==0.14.0" },
|
||||
{ name = "fastuuid", specifier = ">=0.14.0,<1.0" },
|
||||
{ name = "google-cloud-aiplatform", marker = "extra == 'google'", specifier = "==1.133.0" },
|
||||
{ name = "google-cloud-aiplatform", marker = "extra == 'proxy-runtime'", specifier = "==1.133.0" },
|
||||
{ name = "google-cloud-iam", marker = "extra == 'extra-proxy'", specifier = "==2.19.1" },
|
||||
@ -3403,10 +3403,10 @@ requires-dist = [
|
||||
{ name = "grpcio", marker = "extra == 'grpc'", specifier = "==1.78.0" },
|
||||
{ name = "grpcio", marker = "extra == 'proxy-runtime'", specifier = "==1.78.0" },
|
||||
{ name = "gunicorn", marker = "extra == 'proxy'", specifier = "==23.0.0" },
|
||||
{ name = "httpx", specifier = "==0.28.1" },
|
||||
{ name = "importlib-metadata", specifier = "==8.5.0" },
|
||||
{ name = "jinja2", specifier = "==3.1.6" },
|
||||
{ name = "jsonschema", specifier = "==4.23.0" },
|
||||
{ name = "httpx", specifier = ">=0.28.0,<1.0" },
|
||||
{ name = "importlib-metadata", specifier = ">=8.0.0,<9.0" },
|
||||
{ name = "jinja2", specifier = ">=3.1.0,<4.0" },
|
||||
{ name = "jsonschema", specifier = ">=4.0.0,<5.0" },
|
||||
{ name = "langfuse", marker = "extra == 'proxy-runtime'", specifier = "==2.59.7" },
|
||||
{ name = "litellm-enterprise", marker = "extra == 'proxy'", editable = "enterprise" },
|
||||
{ name = "litellm-proxy-extras", marker = "extra == 'proxy'", editable = "litellm-proxy-extras" },
|
||||
@ -3417,7 +3417,7 @@ requires-dist = [
|
||||
{ name = "numpy", marker = "extra == 'stt-nvidia-riva'", specifier = ">=1.26.0" },
|
||||
{ name = "numpydoc", marker = "extra == 'utils'", specifier = "==1.8.0" },
|
||||
{ name = "nvidia-riva-client", marker = "extra == 'stt-nvidia-riva'", specifier = ">=2.15.0" },
|
||||
{ name = "openai", specifier = "==2.33.0" },
|
||||
{ name = "openai", specifier = ">=2.20.0,<3.0.0" },
|
||||
{ name = "opentelemetry-api", marker = "extra == 'proxy-runtime'", specifier = "==1.28.0" },
|
||||
{ name = "opentelemetry-exporter-otlp", marker = "extra == 'proxy-runtime'", specifier = "==1.28.0" },
|
||||
{ name = "opentelemetry-sdk", marker = "extra == 'proxy-runtime'", specifier = "==1.28.0" },
|
||||
@ -3425,12 +3425,12 @@ requires-dist = [
|
||||
{ name = "polars", marker = "extra == 'proxy'", specifier = "==1.38.1" },
|
||||
{ name = "prisma", marker = "extra == 'extra-proxy'", specifier = "==0.11.0" },
|
||||
{ name = "prometheus-client", marker = "extra == 'proxy-runtime'", specifier = "==0.20.0" },
|
||||
{ name = "pydantic", specifier = "==2.12.5" },
|
||||
{ name = "pydantic", specifier = ">=2.10.0,<3.0.0" },
|
||||
{ name = "pyjwt", marker = "extra == 'proxy'", specifier = "==2.12.0" },
|
||||
{ name = "pynacl", marker = "extra == 'proxy'", specifier = "==1.6.2" },
|
||||
{ name = "pypdf", marker = "python_full_version < '3.14' and extra == 'proxy-runtime'", specifier = "==6.10.2" },
|
||||
{ name = "pyroscope-io", marker = "sys_platform != 'win32' and extra == 'proxy'", specifier = "==0.8.16" },
|
||||
{ name = "python-dotenv", specifier = "==1.2.2" },
|
||||
{ name = "python-dotenv", specifier = ">=1.0.0,<2.0" },
|
||||
{ name = "python-multipart", marker = "extra == 'proxy'", specifier = "==0.0.27" },
|
||||
{ name = "pyyaml", marker = "extra == 'proxy'", specifier = "==6.0.3" },
|
||||
{ name = "redisvl", marker = "python_full_version < '3.14' and extra == 'extra-proxy'", specifier = "==0.4.1" },
|
||||
@ -3442,8 +3442,8 @@ requires-dist = [
|
||||
{ name = "sentry-sdk", marker = "extra == 'proxy-runtime'", specifier = "==2.21.0" },
|
||||
{ name = "soundfile", marker = "extra == 'proxy'", specifier = "==0.12.1" },
|
||||
{ name = "soundfile", marker = "extra == 'stt-nvidia-riva'", specifier = ">=0.12.1" },
|
||||
{ name = "tiktoken", specifier = "==0.12.0" },
|
||||
{ name = "tokenizers", specifier = "==0.23.1" },
|
||||
{ name = "tiktoken", specifier = ">=0.8.0,<1.0" },
|
||||
{ name = "tokenizers", specifier = ">=0.21.0,<1.0" },
|
||||
{ name = "uvicorn", marker = "extra == 'proxy'", specifier = "==0.33.0" },
|
||||
{ name = "uvloop", marker = "sys_platform != 'win32' and extra == 'proxy'", specifier = "==0.21.0" },
|
||||
{ name = "websockets", marker = "extra == 'proxy'", specifier = "==15.0.1" },
|
||||
|
||||
Loading…
Reference in New Issue
Block a user