Fail loudly on an unparseable SENTRY_DSN and a malformed .env (closes #283)
All checks were successful
check / check (push) Successful in 3m32s
All checks were successful
check / check (push) Successful in 3m32s
Two configuration paths still failed silently, against the rule every other variable follows: a value that is set but cannot be parsed must abort startup rather than substitute a default. SENTRY_DSN is now parsed in loadFromEnv, with sentry.NewDsn — the same call sentry.Init makes on the DSN it is handed, so configuration and initialisation cannot disagree about what a valid DSN is. That costs internal/config an import of the Sentry SDK, which is already a module dependency already linked into the binary, and buys a single definition of validity rather than a hand-rolled second one free to drift. A typo in a DSN used to log one error line and leave the process serving with error reporting off forever, which nothing downstream can notice: the variable is still set, so every later signal reports it as on. hasSentryDSN is replaced by Config.SentryEnabled(), following MetricsAuthEnabled(): one method read by the startup log field, by the SDK initialisation and by the sentryhttp middleware, so the log cannot report reporting as on while nothing is sending. enableSentry's error branch is now fatal, and Run gives up before it listens rather than binding a port it is about to release. Fatal there means what a listen failure already meant — Shutdowner.Shutdown(fx.ExitCode(1)), through fx's normal stop sequence — so shutdownOnListenFailure is now shutdownWithFailure and ListenFailureExitCode is StartupFailureExitCode. The godotenv/autoload blank import is replaced by config.LoadDotEnv, called at the top of dispatch. autoload discarded Load's error, and godotenv applies nothing at all when a file will not parse, so one mistyped line reverted every variable in the file to its default and started the server with no log line naming the file. A missing file stays fine — it is optional and most deployments have none. The call sits in dispatch rather than in loadFromEnv because autoload ran in an init(), ahead of config.DataDir(), which both the DATA_DIR lock and resetpw call outside the fx graph; loading any later would let a .env that sets DATA_DIR lock one directory while the config opened databases in another. Both defects were reproduced against the previous build first: an unparseable DSN served traffic while logging "hasSentryDSN":true, and a malformed .env started on the default port with the file unmentioned.
This commit is contained in:
@@ -518,8 +518,20 @@ type badEnvValueCase struct {
|
||||
}
|
||||
|
||||
// badEnvValueCases is the config.New table, kept out of the test body
|
||||
// so the test itself stays readable.
|
||||
// so the test itself stays readable. It is assembled from per-variable
|
||||
// groups because one literal covering every variable outgrew the
|
||||
// function-length budget.
|
||||
func badEnvValueCases() []badEnvValueCase {
|
||||
cases := listenerEnvValueCases()
|
||||
cases = append(cases, flagEnvValueCases()...)
|
||||
cases = append(cases, sentryEnvValueCases()...)
|
||||
|
||||
return cases
|
||||
}
|
||||
|
||||
// listenerEnvValueCases covers the two variables that describe the
|
||||
// HTTP listener.
|
||||
func listenerEnvValueCases() []badEnvValueCase {
|
||||
return []badEnvValueCase{
|
||||
{
|
||||
name: "valid PORT is used",
|
||||
@@ -542,27 +554,6 @@ func badEnvValueCases() []badEnvValueCase {
|
||||
value: "70000",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "valid DEBUG is used",
|
||||
key: envKeyDebug,
|
||||
value: "true",
|
||||
check: func(t *testing.T, cfg *config.Config) {
|
||||
t.Helper()
|
||||
assert.True(t, cfg.Debug)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unparseable DEBUG aborts startup",
|
||||
key: envKeyDebug,
|
||||
value: "ture",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "unparseable MAINTENANCE_MODE aborts startup",
|
||||
key: envKeyMaintenanceMode,
|
||||
value: "sometimes",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "valid BIND_ADDRESS is used",
|
||||
key: envKeyBindAddress,
|
||||
@@ -595,6 +586,69 @@ func badEnvValueCases() []badEnvValueCase {
|
||||
}
|
||||
}
|
||||
|
||||
// flagEnvValueCases covers the boolean variables.
|
||||
func flagEnvValueCases() []badEnvValueCase {
|
||||
return []badEnvValueCase{
|
||||
{
|
||||
name: "valid DEBUG is used",
|
||||
key: envKeyDebug,
|
||||
value: "true",
|
||||
check: func(t *testing.T, cfg *config.Config) {
|
||||
t.Helper()
|
||||
assert.True(t, cfg.Debug)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unparseable DEBUG aborts startup",
|
||||
key: envKeyDebug,
|
||||
value: "ture",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "unparseable MAINTENANCE_MODE aborts startup",
|
||||
key: envKeyMaintenanceMode,
|
||||
value: "sometimes",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// sentryEnvValueCases covers SENTRY_DSN. The three rejected values are
|
||||
// the ones measured on the defect: each initialised the SDK with an
|
||||
// error and left the process serving with error reporting off.
|
||||
func sentryEnvValueCases() []badEnvValueCase {
|
||||
return []badEnvValueCase{
|
||||
{
|
||||
name: "valid SENTRY_DSN is used",
|
||||
key: envKeySentryDSN,
|
||||
value: validSentryDSN,
|
||||
check: func(t *testing.T, cfg *config.Config) {
|
||||
t.Helper()
|
||||
assert.Equal(t, validSentryDSN, cfg.SentryDSN)
|
||||
assert.True(t, cfg.SentryEnabled())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unparseable SENTRY_DSN aborts startup",
|
||||
key: envKeySentryDSN,
|
||||
value: "not-a-dsn",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "SENTRY_DSN that is not a URL aborts startup",
|
||||
key: envKeySentryDSN,
|
||||
value: "%%%",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "keyless SENTRY_DSN aborts startup",
|
||||
key: envKeySentryDSN,
|
||||
value: "https://example.invalid/1",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewUsesDefaultsWhenUnset proves the fail-loud behaviour did not
|
||||
// break the legitimate unset case: absent variables still get their
|
||||
// documented defaults.
|
||||
@@ -603,7 +657,7 @@ func TestNewUsesDefaultsWhenUnset(t *testing.T) {
|
||||
|
||||
for _, key := range []string{
|
||||
envKeyPort, envKeyDebug, envKeyMaintenanceMode,
|
||||
envKeyBindAddress,
|
||||
envKeyBindAddress, envKeySentryDSN,
|
||||
} {
|
||||
require.NoError(t, os.Unsetenv(key))
|
||||
}
|
||||
@@ -626,4 +680,9 @@ func TestNewUsesDefaultsWhenUnset(t *testing.T) {
|
||||
t, config.DefaultBindAddressForTest, cfg.BindAddress,
|
||||
)
|
||||
assert.Equal(t, bindAddressDefault, cfg.BindAddress)
|
||||
|
||||
// An absent SENTRY_DSN is the common case and must stay a normal
|
||||
// start with error reporting off, not a refusal.
|
||||
assert.Empty(t, cfg.SentryDSN)
|
||||
assert.False(t, cfg.SentryEnabled())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user