fix(litellm): percent-encode DB password in DATABASE_URL

LiteLLM crash-looped on macOS with Prisma `P1013: invalid port number in
database URL`. The shared auth token is generated by `openssl rand -base64`
and can contain '/', '+' or '='; injected raw into the DATABASE_URL
userinfo, a '/' truncates the authority so the port parses as invalid and
proxy startup fails (port 4000 never binds).

Percent-encode the password for the DATABASE_URL only, via an explicit
reserved-set replace chain ('%' first to avoid double-encoding) since
Jinja's urlencode leaves '/' unescaped. The DB user password stays raw in
provision-database and LITELLM_DB_PASSWORD, and the URL form decodes back
to the identical secret (verified round-trip), so authentication is
unchanged. No effect when no DB host is configured.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Haitao Pan 2026-06-22 12:56:56 +08:00
parent ef67c61cf7
commit 9926a46f76

View File

@ -107,8 +107,22 @@ litellm_database_password: "{{ lookup('ansible.builtin.env', 'LITELLM_DATABASE_P
litellm_database_admin_user: "{{ lookup('ansible.builtin.env', 'LITELLM_DATABASE_ADMIN_USER') | default('postgres', true) }}"
litellm_database_admin_password: "{{ lookup('ansible.builtin.env', 'LITELLM_DATABASE_ADMIN_PASSWORD') | default('', true) }}"
# Percent-encode the password for use inside the DATABASE_URL userinfo. The
# shared auth token is `openssl rand -base64`, which can contain '/', '+' and
# '=' — a raw '/' truncates the URL authority and Prisma aborts with
# "P1013: invalid port number in database URL". Jinja's `urlencode` leaves '/'
# safe, so encode the reserved set explicitly ('%' first to avoid double
# encoding). The actual DB user password stays raw (provision-database and
# LITELLM_DB_PASSWORD use it verbatim); only the URL form is encoded so the
# client decodes back to the same raw secret.
litellm_database_password_urlencoded: >-
{{ litellm_database_password
| replace('%', '%25') | replace('/', '%2F') | replace('+', '%2B')
| replace('=', '%3D') | replace('@', '%40') | replace(':', '%3A')
| replace('?', '%3F') | replace('#', '%23') | replace(' ', '%20') }}
# Build DATABASE_URL from components (used in litellm.env)
litellm_database_url: "{% if litellm_database_host | trim | length > 0 %}postgresql://{{ litellm_database_user }}:{{ litellm_database_password }}@{{ litellm_database_host }}:{{ litellm_database_port }}/{{ litellm_database_name }}?sslmode={{ litellm_database_sslmode }}{% else %}{% endif %}"
litellm_database_url: "{% if litellm_database_host | trim | length > 0 %}postgresql://{{ litellm_database_user }}:{{ litellm_database_password_urlencoded | trim }}@{{ litellm_database_host }}:{{ litellm_database_port }}/{{ litellm_database_name }}?sslmode={{ litellm_database_sslmode }}{% else %}{% endif %}"
# Models are now dynamically managed via DB/UI or user-provided config