fix(config): Datenbank-Passwort mit Sonderzeichen bricht den Start

Die Verbindungszeichenkette wurde mit fmt.Sprintf zusammengeklebt. Ein
Passwort mit % und @ ergab eine kaputte URL:

  parse "postgres://aether:8UoNj%CoWJ7ze@qy@localhost:5432/aether":
  invalid URL escape "%Co"

Der Server kam nicht über die Migrationen hinaus. Die key=value-Form hätte
dasselbe Problem mit einem Leerzeichen gehabt, nur stiller.

config.DSN() liefert jetzt eine postgres://-URL, gebaut mit net/url und
net.JoinHostPort (klammert IPv6). pgxpool und golang-migrate nehmen sie
beide unverändert – dsnToURL, splitDSN, indexByte und orDefault in pkg/db
sind ersatzlos entfallen. Test deckt %, @, :, /, Leerzeichen und IPv6 ab.

Dazu: DB_HOST und DB_PORT fehlten in der schlanken .env.example, weshalb
localhost statt postgres gegriffen hat. APP_ENV ergänzt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 5 committed 2026-08-24 11:20:01 +02:00
1 parent 16ab6f385b
commit 35ebf609b7
5 files changed
+153 -71

No files matched your search

+6 -67
View File
@@ -25,13 +25,17 @@ func Connect(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
return pool, nil
}
// Migrate führt die eingebetteten Migrationen aus.
//
// `dsn` ist die postgres://-URL aus config.DSN(). Hier stand früher eine
// eigene Umrechnung von key=value in eine URL, die Nutzer und Passwort
// unmaskiert hineinschrieb – siehe die Begründung bei config.DSN().
func Migrate(dsn string) error {
src, err := iofs.New(migrationsFS, "migrations")
if err != nil {
return fmt.Errorf("load migrations: %w", err)
}
// convert pgx DSN → lib/pq DSN format for migrate
m, err := migrate.NewWithSourceInstance("iofs", src, "postgres://"+dsnToURL(dsn))
m, err := migrate.NewWithSourceInstance("iofs", src, dsn)
if err != nil {
return fmt.Errorf("init migrate: %w", err)
}
@@ -40,68 +44,3 @@ func Migrate(dsn string) error {
}
return nil
}
// dsnToURL converts "host=... port=... dbname=... user=... password=... sslmode=..."
// to a postgres:// URL for golang-migrate.
func dsnToURL(dsn string) string {
// pgxpool DSN is already accepted by migrate if we prefix with postgres://
// Actually golang-migrate accepts the postgres:// URL format.
// Since our DSN is key=value, we need to convert it.
// Simplest approach: parse key=value pairs.
params := map[string]string{}
for _, part := range splitDSN(dsn) {
if len(part) == 0 {
continue
}
idx := indexByte(part, '=')
if idx < 0 {
continue
}
params[part[:idx]] = part[idx+1:]
}
host := params["host"]
port := params["port"]
if port == "" {
port = "5432"
}
return fmt.Sprintf("%s:%s@%s:%s/%s?sslmode=%s",
params["user"], params["password"],
host, port, params["dbname"],
orDefault(params["sslmode"], "disable"),
)
}
func splitDSN(dsn string) []string {
var parts []string
cur := ""
for _, r := range dsn {
if r == ' ' {
if cur != "" {
parts = append(parts, cur)
cur = ""
}
} else {
cur += string(r)
}
}
if cur != "" {
parts = append(parts, cur)
}
return parts
}
func indexByte(s string, b byte) int {
for i := 0; i < len(s); i++ {
if s[i] == b {
return i
}
}
return -1
}
func orDefault(s, def string) string {
if s == "" {
return def
}
return s
}