Skip to content

Every filesystem error exits 5 (the retry code): syscall.Errno satisfies net.Error, so isNetworkErr matches through *fs.PathError #241

Description

@ZacxDev

Summary

Every filesystem failure in the CLI that is not already tagged exits 5 — the code the README tells scripts to retry on. A permissions problem is not retryable, so a CI job that sleeps-and-retries on 5 loops forever, and civitai login against a read-only config directory reports itself as a network failure.

Mechanism

cmd/civitai/main.go's isNetworkErr ends with:

var netErr net.Error
return errors.As(err, &netErr)

syscall.Errno declares both Timeout() bool and Temporary() bool, so it satisfies net.Error. The filesystem wrapper types do not (measured on go1.25.12 — none of the three declares Temporary()):

type satisfies net.Error?
syscall.Errno yes
*fs.PathError no
*os.LinkError no
*os.SyscallError no

So errors.As unwraps straight past the *fs.PathError that os.ReadFile / os.Stat / os.MkdirAll return and matches the bare Errno underneath it. exitCode's switch reaches isNetworkErr in its default arm, so any untagged filesystem error lands on exitNetwork.

This is not specific to EACCES — it is every errno. Measured with the classifier at origin/main (569f5dc):

errno                        Timeout  Temporary  exitCode(bare)  exitCode(*fs.PathError)
EACCES  permission denied      false     false         5                  5
ENOENT  no such file           false     false         5                  5
ENOTDIR not a directory        false     false         5                  5
EISDIR  is a directory         false     false         5                  5

Timeout()/Temporary() being false makes no difference: satisfying the interface is all errors.As looks at.

Measured on the built binary

origin/main @ 569f5dc, six pure-filesystem invocations, no network involved:

invocation rc stderr
app listing set-icon <mode-000 png> 5 open …/icon.png: permission denied
app listing set-cover <mode-000 png> 5 open …/icon.png: permission denied
app listing add-screenshot <mode-000 png> 5 open …/icon.png: permission denied
generate "…" --ecosystem Qwen --image <mode-000 png> --dry-run 5 --image …: open …: permission denied
app validate <regular-file>/civitai-app.json (ENOTDIR) 5 stat …/block.manifest.json: not a directory
login --token … with an unwritable XDG_CONFIG_HOME 5 mkdir …/config: permission denied

Blast radius

grep -E '\bos\.(ReadFile|Open|OpenFile|Stat|Lstat|MkdirAll|Mkdir|WriteFile|Create|Remove|RemoveAll|Rename|ReadDir|Symlink|Link|Chmod|Truncate|Getwd|UserHomeDir|CreateTemp|MkdirTemp)\(' over non-test internal/, pkg/, cmd/ finds 80 call sites across 24 files (top: internal/cmd/download.go 10, internal/manifest/manifest.go 9, internal/scaffold/scaffold.go 7, internal/cmd/generate_state.go 6, internal/blockproto/entrygraph.go 6). That is a floor, not a ceiling — it does not count filepath.Walk/WalkDir or *os.File method errors, which are also *fs.PathError. Every one of those returning an untagged error inherits the misclassification.

Already inconsistent today

generate --input <unreadable json> exits 2, not 5, because that path is asUsageError-tagged and usage is checked before the network arm. So two filesystem failures one flag apart already disagree — a second argument for fixing this in the classifier rather than per call site.

The repo already knows about this. internal/cmd/app_listing.go's loadAndValidateImage comment says in so many words that the fix "belongs in isNetworkErr — where it corrects every filesystem error in the CLI at once — and not in a per-call-site tag here", and internal/cmd/exitcodes_doc_test.go records the exit-5 behaviour as "a separate pre-existing defect in cmd/civitai's isNetworkErr". This issue is that follow-up.

Fix

Centrally, in the classifier. A syscall.Errno — however it is wrapped — is not evidence of a transport failure; only the net stack's own error types are. Genuine network failures (*net.OpError, *net.DNSError, *url.Error, ECONNREFUSED/ECONNRESET, context.DeadlineExceeded, HTTP 502/503/504 after retries) must keep exiting 5.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions