From 9926a46f76c917cdb71cf6869be14e3843984f03 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Mon, 22 Jun 2026 12:56:56 +0800 Subject: [PATCH] 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 --- roles/vhosts/litellm/defaults/main.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/roles/vhosts/litellm/defaults/main.yml b/roles/vhosts/litellm/defaults/main.yml index ecf9053..9251dda 100644 --- a/roles/vhosts/litellm/defaults/main.yml +++ b/roles/vhosts/litellm/defaults/main.yml @@ -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