fix: treat empty on-disk JSON state files as empty state#616
Draft
mwbrooks wants to merge 3 commits into
Draft
Conversation
… error
Truncated writes to the CLI's JSON state files (credentials.json,
~/.slack/config.json, project .slack/config.json, .slack/apps.json,
.slack/apps.dev.json) can leave a file at zero bytes on disk when a prior
process is interrupted between afero.WriteFile's O_TRUNC and the actual
write. Once that happens, every subsequent CLI invocation reads the same
0-byte file, hits "unexpected end of JSON input" in the read helper, and
raises ErrUnableToParseJSON.
The June 2026 monthly report showed 1,317,610 unable_to_parse_json events
from just 38 users (a hot loop concentrated in a small cohort running
`platform run` and `api` in tight automation loops), which matches this
failure mode: a file that never repairs itself keeps re-triggering on
every invocation.
Add a shared goutils.IsEmptyJSON helper and use it in the five hot
readers (auth credentials, system config, project config, deployed apps,
local dev apps). When the file exists but is empty or whitespace-only,
return the same zero-value state the "file does not exist" branch
already returns — the app_client readers also re-serialise a valid `{}`
so the file self-heals on the next successful write. Genuinely malformed
JSON still returns ErrUnableToParseJSON as before.
Test coverage: added unit tests covering zero-byte and whitespace-only
inputs for each of the five readers, plus a unit test for the
IsEmptyJSON helper itself. Existing "broken JSON" tests are unchanged.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #616 +/- ##
==========================================
+ Coverage 71.72% 71.74% +0.02%
==========================================
Files 226 226
Lines 19182 19204 +22
==========================================
+ Hits 13758 13778 +20
Misses 4214 4214
- Partials 1210 1212 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
This pull request helps to address
unable_to_parse_jsoninternal errors that some users are experiencing.Root cause hypothesis
afero.WriteFileopens withO_TRUNCbefore writing. If the process is interrupted (SIGKILL, host crash, oom, user cancel) after the truncate but before the write completes, the JSON state file is left at zero bytes. Every subsequent CLI invocation then reads the file, callsjson.Unmarshal(nil, …)on the empty contents, and raisesErrUnableToParseJSON. The file never self-heals: the reader errors out before the caller reaches the code path that would rewrite it.Affected state files:
~/.slack/credentials.json~/.slack/config.json.slack/config.json.slack/apps.json.slack/apps.dev.jsonWhat this PR changes
Adds a single helper
goutils.IsEmptyJSON([]byte) booland updates the five readers so that an empty (or whitespace-only) file returns the same zero-value state as the "file does not exist" branch, rather than surfacing a parse error:internal/auth/auth.go—auths()returns an emptyAuthByTeamIDinternal/config/system.go—UserConfigreturns a config withSurveysinitialisedinternal/config/project.go—ReadProjectConfigFilereturns a config withSurveysinitialisedinternal/app/app_client.go—readDeployedAppsandreadLocalAppsreturn empty maps and re-serialise a valid{}so the file self-heals on the next write cycleGenuinely malformed JSON still returns
ErrUnableToParseJSON. Existing broken-JSON tests are untouched.Test plan
Test_IsEmptyJSONcovers empty, whitespace-only,{}, and populated inputsTest_Client_authsempty-file subtest returns no auths, no errorTest_SystemConfig_UserConfigempty-file subtest returns zero-valued config, no errorTest_ReadProjectConfigFileempty-file subtest returns zero-valued config, no errorAppClient.readDeployedAppsempty-file subtest returns empty apps and writes{}back to diskAppClient.readLocalAppsempty-file subtest returns empty local apps and writes{}back to diskgo test ./internal/... ./cmd/...passesmake lintpasses with zero issuesFollow-ups (not in this PR)
afero.WriteFilewith a temp-file +Renamepattern across the five state writers so the durable fix is that these files can never reach a 0-byte state in the first place.goutils(or a newinternal/statefilepackage) so future state files pick up the guard for free.internal/api/activity.go—ErrHTTPResponseInvalidon the activity poll loop shows the same "thousands of events off one broken response" shape; worth a matching guard.internal/update/sdk.go— retry with backoff on persistently malformed SDK hook response so a broken SDK doesn't dominate the error telemetry either.