Fail loudly on an unparseable SENTRY_DSN and a malformed .env (closes #283) #289
Reference in New Issue
Block a user
Delete Branch "issue-283-fail-loud-config"
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?
Closes #283.
Two configuration paths still failed silently, against the rule every other variable follows.
SENTRY_DSN
Parsed in
loadFromEnvby a newenvSentryDSN, shaped likeenvPort/envBindAddressand wrapping a newErrInvalidSentryDSN. It parses withsentry.NewDsn, which is the callsentry.Initmakes on the DSN it is handed, so configuration and initialisation cannot disagree about what a valid DSN is.The trade-off, stated:
internal/confignow imports the Sentry SDK. It is already a module dependency already linked into this binary, so it costs nothing at build time. The alternative — a syntactic check in the config package — is a second definition of "valid DSN" free to drift from the one that actually decides. I took the import.hasSentryDSNis replaced byConfig.SentryEnabled(), followingMetricsAuthEnabled(): one method read by the startup log field (nowsentryEnabled), by the SDK initialisation, and — throughs.sentryEnabled— by thesentryhttpmiddleware registration. The log cannot report reporting as on while nothing is sending.enableSentry's error branch is fatal, comment and all, andRungives up before it listens rather than binding a port it is about to release. Fatal means what a listen failure already meant here:Shutdowner.Shutdown(fx.ExitCode(1))through fx's normal stop sequence — not a panic, not a bareos.Exit, and every stop hook still runs.shutdownOnListenFailureis therefore nowshutdownWithFailureandListenFailureExitCodeisStartupFailureExitCode, since both now describe more than a listen.Worth knowing for review: with config validating via
NewDsn, that error branch is unreachable in this SDK version —sentry.NewClientin v0.25.0 returns an error only fromNewDsn. It stays because that is a property of the SDK's current implementation, not of its contract. The test reaches it by putting an unparseable DSN on a hand-builtConfig, which bypassesloadFromEnv..env
The
godotenv/autoloadblank import is replaced byconfig.LoadDotEnv().autoloaddiscardedLoad's error, and godotenv parses the whole file before setting anything — so one mistyped line applied none of it, reverting every variable in the file to its default with no log line naming the file.A missing file stays fine: it is optional and most deployments have none. Only a file that is there and cannot be read or parsed aborts, naming it. A variable already in the real environment still wins over the file.
Scope disclosure: this needed
cmd/webhooker/main.gotoo, which is outside the fence I was given.LoadDotEnv()is called at the top ofdispatch().autoloadran in aninit(), ahead ofconfig.DataDir()— which both theDATA_DIRlock inrun()andresetpwcall outside the fx graph. Loading only insideloadFromEnv()would move it 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 ahead of every reader of the environment on both subcommand paths. Themain.gochange is 8 lines plus ahelpCommandconstant thatgoconstdemanded once a fourth test used the literal.Verification
make checkgreen withGOFLAGS=-count=1after the rebase onto currentnext(48cf93e). Lint ran uncached in Docker (0 issues, 56.6s, noCACHEDon that layer); no test line reported(cached).Both defects were reproduced against a build of unmodified
nextfirst, then re-checked against the built binary from this branch.SENTRY_DSN=not-a-dsnsentry init failureand"hasSentryDSN":trueinvalid Sentry DSN: SENTRY_DSN: "not-a-dsn": [Sentry] DsnParseError: invalid schemeSENTRY_DSN=%%%SENTRY_DSN=https://example.invalid/1"sentryEnabled":true,sentry error reporting activatedSENTRY_DSNabsent"sentryEnabled":false, listener up.env.env, before any log output and before theDATA_DIRlock.env.envDATA_DIRset only in.envproducedwebhooker.lockandwebhooker.dbin that directory, proving the load still precedes the lockIn the rejected-DSN state no
sentryEnabledfield is emitted at all, because the process never reaches the startup summary.resetpwis refused by a malformed.envon the same terms as the server.Tests added:
TestEnvSentryDSN(10 rows),TestSentryEnabled_TracksTheDSN, fourSENTRY_DSNrows in the existingconfig.Newtable plus the unset case inTestNewUsesDefaultsWhenUnset, sixTestLoadDotEnv_*covering missing / valid / real-environment-wins / malformed / unreadable / working-directory-relative, threeTestDispatch_*pinning that.envis read before any subcommand runs, andTestSentryInitFailure_ShutsDownTheApp, which asserts the non-zero exit, that the stop sequence still completes, and that the port was never bound.badEnvValueCaseswas split into three per-variable groups; the new rows pushed it past thefunlenbudget.README
SENTRY_DSNadded to the "Invalid values abort startup" enumeration, which omitted it, and the table row now says what unset and unparseable each do. The section's unqualified claim is left as it was — the code is now true instead. The.envparagraph no longer namesgodotenv/autoloadand documents the parse behaviour: optional, malformed aborts, real environment wins.Not done
TODO.mduntouched, per instructions.PASS. Both defects fixed against the iron rule; verified by execution, not by reading.
Anomalies/disclosures, none blocking:
helpnow exits 1 on a malformed.env. BecauseLoadDotEnv()sits at the top ofdispatch(),webhooker help(and an unknown subcommand) is refused when.envwill not parse — the one subcommand that reads no config. It is deliberate, documented in theTestDispatch_MalformedDotEnvRefusescomment, and loud rather than silent, so it is not a defect; flagging it as a behaviour change a reader might not expect..env(godotenv.Loaddoes not overwrite). Verified:.envPORT=19713with a realPORT=19714bound127.0.0.1:19714. README documents this.make check/make fmt-checkran in the reviewed tree via themaketargets only, lint in Docker.internal/config, and keepingenableSentry's error branch that is unreachable in sentry-go v0.25.0.