The config env helpers (envInt, and any peers) silently fall back to the default when an env var is set but unparseable — the same silent-failure pattern @sneak flagged for the duration parser: a set-but-invalid config value should fail loudly and prevent startup, not be silently ignored.
Definition of done:
envInt (and any other set-but-unparseable-swallowing helpers) return an error on a set-but-unparseable value
config.New propagates it so startup aborts on bad config
an UNSET value still legitimately uses the default
covered by tests
Note: this is an intentional behaviour change — an existing deployment with a malformed config value will now fail to start rather than run with a silently-substituted default.
Extends the fix on PR #78 (#63).
The config env helpers (`envInt`, and any peers) silently fall back to the default when an env var is set but unparseable — the same silent-failure pattern @sneak flagged for the duration parser: a set-but-invalid config value should fail loudly and prevent startup, not be silently ignored.
Definition of done:
- `envInt` (and any other set-but-unparseable-swallowing helpers) return an error on a set-but-unparseable value
- `config.New` propagates it so startup aborts on bad config
- an UNSET value still legitimately uses the default
- covered by tests
Note: this is an intentional behaviour change — an existing deployment with a malformed config value will now fail to start rather than run with a silently-substituted default.
Baseline: main @ 4f5ecb1. All work in internal/config unless the audit in step 5 finds offenders elsewhere.
Policy being encoded
A set-but-invalid environment value aborts startup. Defaults apply only to keys that are unset (or empty). There is no third behaviour — never silently substitute a default for a value the operator actually set.
1. Add envPositiveInt
Add it verbatim as PR #87 defines it on issue-64-receiver-rate-limit — same function name, same signature envPositiveInt(key string, defaultValue int) (int, error), same package-level ErrNonPositiveValue, same error strings:
parses to less than 1 -> 0, fmt.Errorf("%w: %s must be at least 1, got %q", ErrNonPositiveValue, key, v)
Copying it byte-for-byte is deliberate: #87 is already merge-ready and will very likely land first, so an identical definition makes the rebase a trivial delete-one-copy instead of a semantic merge.
2. envInt must stop swallowing
envInt currently returns defaultValue when the value is set but strconv.Atoi fails. Its only caller is PORT. Fix by removing envInt entirely and parsing PORT through envPositiveInt, plus an explicit upper-bound check: a TCP port must be in 1..65535. Add a package-level ErrInvalidPort for the out-of-range case with an error naming the key and the bad value. If you keep envInt for any reason, it must return (int, error) — a silent-fallback variant must not survive anywhere in the package.
3. envBool must stop swallowing
envBool currently treats every non-empty value that is not true/1 as false, so DEBUG=yes, DEBUG=on, and a typo like DEBUG=ture all silently disable debug. Change it to envBool(key string, defaultValue bool) (bool, error):
unset/empty -> defaultValue, nil
otherwise parse with strconv.ParseBool (stdlib, per our library policy — do not hand-roll a spelling table)
parse failure -> a wrapped error naming the key and the bad value
Callers: DEBUG, MAINTENANCE_MODE. Yes, this narrows the accepted set (mixed-case oddities like TrUe that EqualFold used to accept now fail) — that is the intended fail-loud behaviour, and strconv.ParseBool accepts 1 t T TRUE true True 0 f F FALSE false False.
4. envString and envDuration
envString parses nothing and needs no change; envDuration is already fail-loud (#78). Leave both alone, but say explicitly in the PR body that they were audited, so the reviewer does not have to re-derive it.
5. Audit beyond internal/config
Grep the whole repo for os.Getenv / LookupEnv and check every parse site for the same silent-fallback pattern. Fix any you find under this issue, or state in the PR body that there are none outside internal/config. Do not leave the audit undone.
6. Propagation
config.New returns the first error it hits so fx aborts startup. Errors must name the offending key and the offending value.
7. Tests
Table-driven, one table per helper, three classes each: unset -> default, valid -> parsed, set-but-invalid -> error. Plus at least one config.New-level test proving a bad PORT and a bad DEBUG each abort with an error rather than returning a config. Use t.Setenv so nothing leaks between tests.
8. Docs
README.md: state in the configuration section that any environment variable that is set but unparseable aborts startup, and that defaults apply only to unset variables. List the accepted boolean spellings.
TODO.md: update in the same commit as the code.
Definition of done
No helper in internal/config silently falls back to a default for a set-but-invalid value.
An unset variable still gets its default.
make check green; validated via the repo's own entrypoints only (make check / script/cibuild) — never raw go/golangci-lint.
.golangci.yml untouched (org-standard, pinned on main via #86).
Single commit, title ending in (closes #80).
No attribution trailers.
## Implementation requirements
Baseline: `main` @ `4f5ecb1`. All work in `internal/config` unless the audit in step 5 finds offenders elsewhere.
### Policy being encoded
A set-but-invalid environment value **aborts startup**. Defaults apply **only** to keys that are unset (or empty). There is no third behaviour — never silently substitute a default for a value the operator actually set.
### 1. Add `envPositiveInt`
Add it **verbatim** as PR #87 defines it on `issue-64-receiver-rate-limit` — same function name, same signature `envPositiveInt(key string, defaultValue int) (int, error)`, same package-level `ErrNonPositiveValue`, same error strings:
- unset/empty -> `defaultValue, nil`
- unparseable -> `0, fmt.Errorf("invalid integer for %s: %q: %w", key, v, err)`
- parses to less than 1 -> `0, fmt.Errorf("%w: %s must be at least 1, got %q", ErrNonPositiveValue, key, v)`
Copying it byte-for-byte is deliberate: #87 is already merge-ready and will very likely land first, so an identical definition makes the rebase a trivial delete-one-copy instead of a semantic merge.
### 2. `envInt` must stop swallowing
`envInt` currently returns `defaultValue` when the value is set but `strconv.Atoi` fails. Its only caller is `PORT`. Fix by removing `envInt` entirely and parsing `PORT` through `envPositiveInt`, plus an explicit upper-bound check: a TCP port must be in `1..65535`. Add a package-level `ErrInvalidPort` for the out-of-range case with an error naming the key and the bad value. If you keep `envInt` for any reason, it must return `(int, error)` — a silent-fallback variant must not survive anywhere in the package.
### 3. `envBool` must stop swallowing
`envBool` currently treats every non-empty value that is not `true`/`1` as `false`, so `DEBUG=yes`, `DEBUG=on`, and a typo like `DEBUG=ture` all silently disable debug. Change it to `envBool(key string, defaultValue bool) (bool, error)`:
- unset/empty -> `defaultValue, nil`
- otherwise parse with `strconv.ParseBool` (stdlib, per our library policy — do not hand-roll a spelling table)
- parse failure -> a wrapped error naming the key and the bad value
Callers: `DEBUG`, `MAINTENANCE_MODE`. Yes, this narrows the accepted set (mixed-case oddities like `TrUe` that `EqualFold` used to accept now fail) — that is the intended fail-loud behaviour, and `strconv.ParseBool` accepts `1 t T TRUE true True 0 f F FALSE false False`.
### 4. `envString` and `envDuration`
`envString` parses nothing and needs no change; `envDuration` is already fail-loud (#78). Leave both alone, but say explicitly in the PR body that they were audited, so the reviewer does not have to re-derive it.
### 5. Audit beyond `internal/config`
Grep the whole repo for `os.Getenv` / `LookupEnv` and check every parse site for the same silent-fallback pattern. Fix any you find under this issue, or state in the PR body that there are none outside `internal/config`. Do not leave the audit undone.
### 6. Propagation
`config.New` returns the first error it hits so fx aborts startup. Errors must name the offending key and the offending value.
### 7. Tests
Table-driven, one table per helper, three classes each: unset -> default, valid -> parsed, set-but-invalid -> error. Plus at least one `config.New`-level test proving a bad `PORT` and a bad `DEBUG` each abort with an error rather than returning a config. Use `t.Setenv` so nothing leaks between tests.
### 8. Docs
- `README.md`: state in the configuration section that any environment variable that is set but unparseable aborts startup, and that defaults apply only to unset variables. List the accepted boolean spellings.
- `TODO.md`: update in the **same commit** as the code.
### Definition of done
- No helper in `internal/config` silently falls back to a default for a set-but-invalid value.
- An unset variable still gets its default.
- `make check` green; validated via the repo's own entrypoints only (`make check` / `script/cibuild`) — never raw `go`/`golangci-lint`.
- `.golangci.yml` untouched (org-standard, pinned on `main` via #86).
- Single commit, title ending in ` (closes #80)`.
- No attribution trailers.
Branch issue-80-config-fail-loud off main @ 4f5ecb1, single commit
titled with (closes #80).
internal/config/config.go: add envPositiveInt(key string, defaultValue int) (int, error) byte-for-byte as PR #87 defines it,
with the same package-level ErrNonPositiveValue and the same error
strings, so a rebase after #87 lands is a delete-one-copy operation.
Delete envInt entirely. PORT is parsed through a new envPort
wrapper: envPositiveInt for the unset/unparseable/<1 cases plus an
explicit upper bound (maxPort = 65535) returning a new package-level ErrInvalidPort naming the key and the bad value.
Rewrite envBool as envBool(key string, defaultValue bool) (bool, error) using strconv.ParseBool; unset/empty keeps the default,
anything else that fails to parse is a wrapped error naming the key
and value. Callers DEBUG and MAINTENANCE_MODE updated.
envString and envDuration audited and left alone (envString
parses nothing; envDuration was already made fail-loud in #78) —
stated explicitly in the PR body.
Repo-wide audit of os.Getenv/os.LookupEnv across tracked files;
result recorded in the PR body.
config.New returns the first error, so fx aborts startup. If the
added error handling pushes New past the funlen threshold, the WEBHOOKER_ENVIRONMENT block is extracted into resolveEnvironment,
again copied verbatim from #87 for the same rebase reason.
Tests: table-driven per-helper tables for envPositiveInt, envPort
and envBool (unset -> default, valid -> parsed, set-but-invalid ->
error) reaching the unexported helpers through an export_test.go
shim, plus config.New-level tests proving a bad PORT and a bad DEBUG each abort with an error instead of returning a config. All
env manipulation via t.Setenv.
Docs: README.md configuration section gains the fail-loud rule
(defaults apply only to unset variables) and the list of accepted
boolean spellings; TODO.md updated in the same commit; make fmt
run on the changed markdown.
Verification: make fmt then make check only — no raw go or golangci-lint invocations. .golangci.yml untouched.
## Implementation plan
Branch `issue-80-config-fail-loud` off `main` @ `4f5ecb1`, single commit
titled with ` (closes #80)`.
1. `internal/config/config.go`: add `envPositiveInt(key string,
defaultValue int) (int, error)` byte-for-byte as PR #87 defines it,
with the same package-level `ErrNonPositiveValue` and the same error
strings, so a rebase after #87 lands is a delete-one-copy operation.
2. Delete `envInt` entirely. `PORT` is parsed through a new `envPort`
wrapper: `envPositiveInt` for the unset/unparseable/<1 cases plus an
explicit upper bound (`maxPort = 65535`) returning a new package-level
`ErrInvalidPort` naming the key and the bad value.
3. Rewrite `envBool` as `envBool(key string, defaultValue bool) (bool,
error)` using `strconv.ParseBool`; unset/empty keeps the default,
anything else that fails to parse is a wrapped error naming the key
and value. Callers `DEBUG` and `MAINTENANCE_MODE` updated.
4. `envString` and `envDuration` audited and left alone (`envString`
parses nothing; `envDuration` was already made fail-loud in #78) —
stated explicitly in the PR body.
5. Repo-wide audit of `os.Getenv`/`os.LookupEnv` across tracked files;
result recorded in the PR body.
6. `config.New` returns the first error, so fx aborts startup. If the
added error handling pushes `New` past the `funlen` threshold, the
`WEBHOOKER_ENVIRONMENT` block is extracted into `resolveEnvironment`,
again copied verbatim from #87 for the same rebase reason.
7. Tests: table-driven per-helper tables for `envPositiveInt`, `envPort`
and `envBool` (unset -> default, valid -> parsed, set-but-invalid ->
error) reaching the unexported helpers through an `export_test.go`
shim, plus `config.New`-level tests proving a bad `PORT` and a bad
`DEBUG` each abort with an error instead of returning a config. All
env manipulation via `t.Setenv`.
8. Docs: `README.md` configuration section gains the fail-loud rule
(defaults apply only to unset variables) and the list of accepted
boolean spellings; `TODO.md` updated in the same commit; `make fmt`
run on the changed markdown.
Verification: `make fmt` then `make check` only — no raw `go` or
`golangci-lint` invocations. `.golangci.yml` untouched.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Extends the fix on PR #78 (#63).
The config env helpers (
envInt, and any peers) silently fall back to the default when an env var is set but unparseable — the same silent-failure pattern @sneak flagged for the duration parser: a set-but-invalid config value should fail loudly and prevent startup, not be silently ignored.Definition of done:
envInt(and any other set-but-unparseable-swallowing helpers) return an error on a set-but-unparseable valueconfig.Newpropagates it so startup aborts on bad configNote: this is an intentional behaviour change — an existing deployment with a malformed config value will now fail to start rather than run with a silently-substituted default.
clawbot referenced this issue2026-08-07 18:52:09 +02:00
Implementation requirements
Baseline:
main@4f5ecb1. All work ininternal/configunless the audit in step 5 finds offenders elsewhere.Policy being encoded
A set-but-invalid environment value aborts startup. Defaults apply only to keys that are unset (or empty). There is no third behaviour — never silently substitute a default for a value the operator actually set.
1. Add
envPositiveIntAdd it verbatim as PR #87 defines it on
issue-64-receiver-rate-limit— same function name, same signatureenvPositiveInt(key string, defaultValue int) (int, error), same package-levelErrNonPositiveValue, same error strings:defaultValue, nil0, fmt.Errorf("invalid integer for %s: %q: %w", key, v, err)0, fmt.Errorf("%w: %s must be at least 1, got %q", ErrNonPositiveValue, key, v)Copying it byte-for-byte is deliberate: #87 is already merge-ready and will very likely land first, so an identical definition makes the rebase a trivial delete-one-copy instead of a semantic merge.
2.
envIntmust stop swallowingenvIntcurrently returnsdefaultValuewhen the value is set butstrconv.Atoifails. Its only caller isPORT. Fix by removingenvIntentirely and parsingPORTthroughenvPositiveInt, plus an explicit upper-bound check: a TCP port must be in1..65535. Add a package-levelErrInvalidPortfor the out-of-range case with an error naming the key and the bad value. If you keepenvIntfor any reason, it must return(int, error)— a silent-fallback variant must not survive anywhere in the package.3.
envBoolmust stop swallowingenvBoolcurrently treats every non-empty value that is nottrue/1asfalse, soDEBUG=yes,DEBUG=on, and a typo likeDEBUG=tureall silently disable debug. Change it toenvBool(key string, defaultValue bool) (bool, error):defaultValue, nilstrconv.ParseBool(stdlib, per our library policy — do not hand-roll a spelling table)Callers:
DEBUG,MAINTENANCE_MODE. Yes, this narrows the accepted set (mixed-case oddities likeTrUethatEqualFoldused to accept now fail) — that is the intended fail-loud behaviour, andstrconv.ParseBoolaccepts1 t T TRUE true True 0 f F FALSE false False.4.
envStringandenvDurationenvStringparses nothing and needs no change;envDurationis already fail-loud (#78). Leave both alone, but say explicitly in the PR body that they were audited, so the reviewer does not have to re-derive it.5. Audit beyond
internal/configGrep the whole repo for
os.Getenv/LookupEnvand check every parse site for the same silent-fallback pattern. Fix any you find under this issue, or state in the PR body that there are none outsideinternal/config. Do not leave the audit undone.6. Propagation
config.Newreturns the first error it hits so fx aborts startup. Errors must name the offending key and the offending value.7. Tests
Table-driven, one table per helper, three classes each: unset -> default, valid -> parsed, set-but-invalid -> error. Plus at least one
config.New-level test proving a badPORTand a badDEBUGeach abort with an error rather than returning a config. Uset.Setenvso nothing leaks between tests.8. Docs
README.md: state in the configuration section that any environment variable that is set but unparseable aborts startup, and that defaults apply only to unset variables. List the accepted boolean spellings.TODO.md: update in the same commit as the code.Definition of done
internal/configsilently falls back to a default for a set-but-invalid value.make checkgreen; validated via the repo's own entrypoints only (make check/script/cibuild) — never rawgo/golangci-lint..golangci.ymluntouched (org-standard, pinned onmainvia #86).(closes #80).Implementation plan
Branch
issue-80-config-fail-loudoffmain@4f5ecb1, single committitled with
(closes #80).internal/config/config.go: addenvPositiveInt(key string, defaultValue int) (int, error)byte-for-byte as PR #87 defines it,with the same package-level
ErrNonPositiveValueand the same errorstrings, so a rebase after #87 lands is a delete-one-copy operation.
envIntentirely.PORTis parsed through a newenvPortwrapper:
envPositiveIntfor the unset/unparseable/<1 cases plus anexplicit upper bound (
maxPort = 65535) returning a new package-levelErrInvalidPortnaming the key and the bad value.envBoolasenvBool(key string, defaultValue bool) (bool, error)usingstrconv.ParseBool; unset/empty keeps the default,anything else that fails to parse is a wrapped error naming the key
and value. Callers
DEBUGandMAINTENANCE_MODEupdated.envStringandenvDurationaudited and left alone (envStringparses nothing;
envDurationwas already made fail-loud in #78) —stated explicitly in the PR body.
os.Getenv/os.LookupEnvacross tracked files;result recorded in the PR body.
config.Newreturns the first error, so fx aborts startup. If theadded error handling pushes
Newpast thefunlenthreshold, theWEBHOOKER_ENVIRONMENTblock is extracted intoresolveEnvironment,again copied verbatim from #87 for the same rebase reason.
envPositiveInt,envPortand
envBool(unset -> default, valid -> parsed, set-but-invalid ->error) reaching the unexported helpers through an
export_test.goshim, plus
config.New-level tests proving a badPORTand a badDEBUGeach abort with an error instead of returning a config. Allenv manipulation via
t.Setenv.README.mdconfiguration section gains the fail-loud rule(defaults apply only to unset variables) and the list of accepted
boolean spellings;
TODO.mdupdated in the same commit;make fmtrun on the changed markdown.
Verification:
make fmtthenmake checkonly — no rawgoorgolangci-lintinvocations..golangci.ymluntouched.clawbot referenced this issue2026-08-09 04:33:24 +02:00
clawbot referenced this issue2026-08-11 14:48:08 +02:00