Two config paths fail silently: an unparseable SENTRY_DSN starts anyway, and a malformed .env is discarded whole #283
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found by a config-matrix audit that tested all 13 environment variables across absent / set-and-valid / set-but-invalid. Eleven variables and the
METRICS_*pair all abort correctly. These two do not.Both violate the same rule, stated by the owner on #78: "this should fail loudly and prevent server startup if it cant parse the duration string in the env var. your implementation silently fails. bad."
1.
SENTRY_DSN— set but unparseable, starts anywaySENTRY_DSN=not-a-dsn,%%%, andhttps://example.invalid/1each log{"level":"ERROR","msg":"sentry init failure","error":"[Sentry] DsnParseError: invalid scheme"}and then keep running, serving traffic with error reporting silently off.internal/server/server.go:161-164, with the intent stated in the code:The aggravating part: the startup summary logs
"hasSentryDSN":truein exactly this case. The operator is told Sentry is configured while it is inert. That is the identical failure modeMetricsAuthEnabled()was written to eliminate — its doc comment atconfig.go:161-174argues specifically that the log must not be able to disagree with reality.hasSentryDSNis that same bug, unfixed.This is the "typo in the DSN and you lose all production error reporting, forever, silently" case. The variable exists solely to make failures visible.
2. A malformed
.envis silently discarded in fullA
.envcontaining bad syntax (PORT 19423,this is not = valid ! syntax,"unclosed) results in: server starts, every value in the file ignored, defaults applied, and not one line of log output mentioning the file.github.com/joho/godotenv/autoload(imported atconfig.go:20) callsgodotenv.Load()in itsinit()and discards the returned error, soconfig.gonever sees it.Blast radius is wider than #1: one fat-fingered line loses the ENTIRE file, reverting every variable in it to defaults, with no signal. That defeats the loud-failure guarantee for all 13 variables at once whenever
.envis the delivery mechanism — and README line 73 tells developers to use.env.Why one issue
Both are the same defect class in the same subsystem, and fixing them separately would put two workers in
internal/config. One coherent change.Definition of done
SENTRY_DSNaborts startup, naming the variable, matching the existingenvDuration/envPortpattern. Validate inloadFromEnv()rather than at Sentry-init time, so the failure happens where every other config failure happens.enableSentry's error branch becomes a hard failure, and the comment justifying the current behaviour goes with it.hasSentryDSNis replaced by something that cannot disagree with reality — followMetricsAuthEnabled(), which already solved this exact problem..envfails loudly. This means dropping theautoloadimport for an explicitgodotenv.Load()whose error is handled. Decide and state whether a MISSING.envremains fine (it must — it is optional) while a malformed one aborts.SENTRY_DSNto the per-variable enumeration, which omits it entirely..envparse behaviour where.envis introduced.Verification
SENTRY_DSN=not-a-dsnexits nonzero naming the variable. A valid DSN still starts and activates..envexits nonzero naming the file; a missing.envstill starts; a valid.envstill takes effect.make checkgreen.Both reproduced on unmodified
next(8d64259) before touching anything:SENTRY_DSN=not-a-dsnloggedsentry init failureplus"hasSentryDSN":trueand served until killed; a.envofPORT 19611/this is not = valid ! syntax/"unclosedstarted on the default port 8080 with no log line naming the file.Plan:
SENTRY_DSNvalidated inloadFromEnv()via a newenvSentryDSNhelper shaped likeenvPort/envBindAddress, wrapping a newErrInvalidSentryDSN. It parses withsentry.NewDsn, which is literally the callsentry.Initmakes on the DSN — so the config-time check and the init-time check cannot disagree. Cost:internal/configimportsgithub.com/getsentry/sentry-go. Choosing that over a hand-rolled syntactic check because a syntactic check is a second, drifting definition of "valid DSN", and the SDK is already a module dependency already linked into this binary, so it adds nothing to the build. Unset stays unset — Sentry off, normal start.hasSentryDSNreplaced byConfig.SentryEnabled(), followingMetricsAuthEnabled(): one method, read by both the startup log field (nowsentryEnabled) and byenableSentry, so the log cannot say configured while the subsystem is inert.enableSentry's error branch becomes fatal, comment and all. It runs on the serving goroutine after fx has already reported RUNNING, so "fatal" there means the same thing a listen failure already means in this file:Shutdowner.Shutdown(fx.ExitCode(1))— non-zero exit through fx's normal stop sequence, not a panic and not a bareos.Exit. I will factor the existingshutdownOnListenFailurebody into a shared helper both call..env: drop thegodotenv/autoloadblank import for an exportedconfig.LoadDotEnv(). Missing file (fs.ErrNotExist) returns nil — optional, and the common case. Anything else — parse error, unreadable, a directory — is a wrapped error naming.env.Scope disclosure on (4): this needs a few lines in
cmd/webhooker/main.goas well, callingLoadDotEnv()at the top ofdispatch().autoloadran ininit(), i.e. beforerun()callsconfig.DataDir()to take theDATA_DIRlock and beforeresetpwcalls it. Loading only insideloadFromEnv()would move that after the lock, so a.envsettingDATA_DIRwould lock one directory while the config opened databases in another, andresetpwwould never see the file at all.dispatch()is the one point that precedes every reader of the environment on both subcommand paths.Verification will cover:
not-a-dsn,%%%,https://example.invalid/1each exiting non-zero namingSENTRY_DSN; a valid DSN starting and activating; absent starting with Sentry off; malformed.envexiting non-zero naming the file; missing.envstarting; a valid.envvalue provably reaching the config; andsentryEnabledcorrect in all three states.Built as #289, branch
issue-283-fail-loud-config, basenext.Both defects fixed as planned. Two things worth reading in the PR rather than repeating here: the Sentry SDK import into
internal/config(taken deliberately, reasoning stated), and thecmd/webhooker/main.gochange, which was outside my fence but is what keeps.envahead of theDATA_DIRlock.Verified against a built binary, all eight states:
not-a-dsn,%%%andhttps://example.invalid/1each exit 1 namingSENTRY_DSNand quoting the value; a valid DSN starts with"sentryEnabled":trueandsentry error reporting activated; an absent one starts with"sentryEnabled":false. In the rejected state nosentryEnabledfield is emitted at all — the process never reaches the startup summary — so no field can report a subsystem as configured while it is inert. A malformed.envexits 1 with one stderr line naming the file, before any log output and before the lock; a missing one starts; a valid one takes effect, shown by aDATA_DIRset only in.envproducingwebhooker.lockandwebhooker.dbin that directory, which also proves the load still precedes the lock.resetpwis refused on the same terms.Both defects were reproduced on unmodified
nextfirst, andmake checkis green withGOFLAGS=-count=1after rebasing onto currentnext(48cf93e), with lint running uncached in Docker.