Found while reviewing #880 and #907; out of scope for both. Reproduced by reading, not by
running. Install observation pending.
What happens
POST /api/v1/configuration/startupconfig binds the whole StartupConfig and hands it
straight to the writer. There is no merge with what is already on disk:
StartupConfigurationController.SaveStartupConfig normalises ApiVersion and calls
IConfigurationService.SaveStartupConfigAsync(config).
ConfigurationService.SaveStartupConfigAsync checks the auth-enable lockout guard and
calls startupConfigService.SaveAsync(config).
StartupConfigService.SaveAsync assigns _config = config and writes the serialised
object over the file.
So every field absent from the posted body is gone from config.json: the API key, the bind
address, the port, the SSL settings, the log level.
The SPA reaches that path on every settings save. SettingsView spreads whatever it holds
in its startupConfig ref into the outgoing body, and that ref starts as null and is
filled in onMounted from GET /startupconfig. If that GET failed, the ref is still null
when the user presses Save, original falls back to {}, and the body posted is
{ authenticationRequired: 'false' } and nothing else.
Why it matters more now
Two open changes add fields to StartupConfig that only live in that file:
Options
- Merge server-side.
SaveAsync reads the current config and overlays only the
properties present in the request. Needs a way to tell "absent" from "explicitly null",
so the DTO wants nullable-with-presence semantics (JsonElement, or an explicit patch
DTO) rather than the domain object bound directly. Changes the endpoint's contract from
replace to merge, which is a documented behaviour change but the one that matches what
every caller actually wants. Recommended.
- Guard the client only.
SettingsView refuses to post a startup config it never
successfully loaded, and surfaces the load failure. Smallest change, fixes the SPA, and
leaves the endpoint able to erase a config for anyone using it directly or through a
Readarr-shaped client.
- Both. The client guard is cheap and independently correct; the server merge is what
makes the endpoint safe for the API key it holds. Doing 2 first and 1 as a follow-up is a
reasonable split if the contract change wants its own discussion.
Not proposed
Reconstructing a lost API key silently on the next load. StartupConfigService already
generates one when the key is missing, which means a wipe is survivable but hands every
existing client a changed key with no notice.
Disclosure: drafted with Claude Code at my direction; I read the cited code at the stated commit and reviewed this before posting.
Found while reviewing #880 and #907; out of scope for both. Reproduced by reading, not by
running. Install observation pending.
What happens
POST /api/v1/configuration/startupconfigbinds the wholeStartupConfigand hands itstraight to the writer. There is no merge with what is already on disk:
StartupConfigurationController.SaveStartupConfignormalisesApiVersionand callsIConfigurationService.SaveStartupConfigAsync(config).ConfigurationService.SaveStartupConfigAsyncchecks the auth-enable lockout guard andcalls
startupConfigService.SaveAsync(config).StartupConfigService.SaveAsyncassigns_config = configand writes the serialisedobject over the file.
So every field absent from the posted body is gone from config.json: the API key, the bind
address, the port, the SSL settings, the log level.
The SPA reaches that path on every settings save.
SettingsViewspreads whatever it holdsin its
startupConfigref into the outgoing body, and that ref starts asnulland isfilled in
onMountedfromGET /startupconfig. If that GET failed, the ref is still nullwhen the user presses Save,
originalfalls back to{}, and the body posted is{ authenticationRequired: 'false' }and nothing else.Why it matters more now
Two open changes add fields to
StartupConfigthat only live in that file:ApplicationUrl, the absolute external URL for notification links and images.UrlBasesettable from the UI, so the settings save path starts being useddeliberately for values a reverse proxy depends on.
Any client that posts a partial startup config drops both. The SPA happens to survive today
because a JavaScript object spread carries keys the TypeScript type does not declare, but
that is an accident of the client rather than a property of the endpoint.
Options
SaveAsyncreads the current config and overlays only theproperties present in the request. Needs a way to tell "absent" from "explicitly null",
so the DTO wants nullable-with-presence semantics (
JsonElement, or an explicit patchDTO) rather than the domain object bound directly. Changes the endpoint's contract from
replace to merge, which is a documented behaviour change but the one that matches what
every caller actually wants. Recommended.
SettingsViewrefuses to post a startup config it neversuccessfully loaded, and surfaces the load failure. Smallest change, fixes the SPA, and
leaves the endpoint able to erase a config for anyone using it directly or through a
Readarr-shaped client.
makes the endpoint safe for the API key it holds. Doing 2 first and 1 as a follow-up is a
reasonable split if the contract change wants its own discussion.
Not proposed
Reconstructing a lost API key silently on the next load.
StartupConfigServicealreadygenerates one when the key is missing, which means a wipe is survivable but hands every
existing client a changed key with no notice.
Disclosure: drafted with Claude Code at my direction; I read the cited code at the stated commit and reviewed this before posting.