Skip to content

fix(smr): surface upload failures instead of silently succeeding#85

Open
mellox wants to merge 1 commit into
satisfactorymodding:masterfrom
mellox:fix/smr-upload-error-handling
Open

fix(smr): surface upload failures instead of silently succeeding#85
mellox wants to merge 1 commit into
satisfactorymodding:masterfrom
mellox:fix/smr-upload-error-handling

Conversation

@mellox

@mellox mellox commented Jul 14, 2026

Copy link
Copy Markdown

Problem

ficsit smr upload had several paths that swallowed errors and reported success, so a failed upload could publish a broken or incomplete version while the command still exited 0.

Changes

  • Chunk upload ignored the response. The per-chunk request discarded the *http.Response entirely — HTTP status, the GraphQL errors array, and the uploadVersionPart result were all unchecked, and the body was never closed. A failed chunk (bad token, 413, a GraphQL error) was silently ignored and finalize proceeded on a broken upload. Extracted uploadChunk / sendChunkOnce, which now check the HTTP status, the GraphQL errors array, and the uploadVersionPart boolean, and always close the body.
  • Per-chunk retry for transient failures. Transport errors (e.g. a dropped/corrupted TLS record, connection reset) and 5xx responses are now retried with exponential backoff via avast/retry-go (already a dependency). Permanent failures — 4xx, a GraphQL errors array, or an explicit uploadVersionPart: false — are marked retry.Unrecoverable and not retried.
  • Short read / FD leak. The chunk read used a bare f.Read, which is not guaranteed to fill the buffer and could ship a zero-padded (corrupt) chunk; the file was also opened once per chunk and never closed. Now uses io.ReadFull and opens the file once with a single defer Close.
  • Validate the upload file is a real .smod/.zip archive before creating a version (replaces the existing TODO).
  • Exit non-zero on a failed finalize. The post-finalize state check returned nil on error, so the command exited 0 even when the server rejected the version. It now returns the error.

Testing

  • Unit tests (cmd/smr/upload_test.go) cover: HTTP error status surfacing, a GraphQL errors array surfacing, explicit success, a missing success field (fail-closed), explicit false rejection, a wire-level multipart parity check, the retry path (transient 5xx retried then succeeds), the short-read fix, and archive validation (accept/reject by extension and by zip validity).
  • End-to-end against the live API. A large multi-chunk upload (~30 × 10 MB) was run repeatedly. The retry layer transparently recovered from real transient failures mid-upload (a tls: bad record MAC and a connection reset on individual chunks) and completed all chunks — where the previous code would have silently dropped a chunk and finalized a corrupt version. The finalize state-check change also surfaced a real server-side finalize error that the old code swallowed as a successful (exit 0) run.

Notes

  • avast/retry-go was already listed in go.mod; no new dependency added.
  • go build, go vet, gofmt, and go test ./... are clean.

`ficsit smr upload` had several paths that swallowed errors and reported
success, so a failed upload could publish a broken/incomplete version while the
command still exited 0.

- Chunk upload discarded the HTTP response entirely: status code, GraphQL
  `errors`, and the `uploadVersionPart` result were all unchecked, and the body
  was never closed. A failed chunk was silently ignored and finalize proceeded.
  Extracted `uploadChunk`/`sendChunkOnce`, which now check HTTP status, the
  GraphQL `errors` array, and the `uploadVersionPart` boolean, and close the body.
- Added per-chunk retry for transient failures (transport errors such as a
  dropped/corrupted TLS record, and 5xx) with exponential backoff via
  avast/retry-go (already a dependency). 4xx, GraphQL errors, and an explicit
  `uploadVersionPart: false` are treated as permanent and not retried.
- Chunk read used a bare `f.Read` (not guaranteed to fill the buffer, so it
  could ship a zero-padded/corrupt chunk) and reopened + leaked the file each
  iteration; now uses `io.ReadFull` and opens the file once with a single defer.
- Validate the upload file is a real `.smod`/`.zip` archive before creating a
  version (previously a TODO).
- The post-finalize state check returned `nil` on error, so the command exited 0
  even when the server rejected the version; it now returns the error.

Adds unit tests for the chunk error/response handling, the retry path, the
short-read fix, and archive validation.
@mellox
mellox marked this pull request as ready for review July 14, 2026 21:55
Copilot AI review requested due to automatic review settings July 14, 2026 21:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens ficsit smr upload so chunk upload and finalize failures are surfaced (non-zero exit) instead of being silently ignored, reducing the risk of publishing incomplete/corrupt versions.

Changes:

  • Refactors chunk uploading into helpers that validate HTTP status + GraphQL errors + uploadVersionPart result and always close response bodies.
  • Adds per-chunk retries with exponential backoff for transient transport/5xx failures.
  • Fixes chunk reading (no silent short reads), validates upload archives as real .smod/.zip, and adds unit test coverage for these paths.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
cmd/smr/upload.go Adds validated chunk upload + retry/backoff, fail-closed response handling, robust chunk reads, archive validation, and proper error surfacing on finalize state-check.
cmd/smr/upload_test.go Adds unit tests covering chunk upload error surfacing, retry behavior, multipart parity, short-read handling, and archive validation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cmd/smr/upload.go
bufferSize = stat.Size() - offset
}

chunk := make([]byte, bufferSize)
Comment thread cmd/smr/upload.go
Comment on lines +81 to +83
if err := validateSmodFile(filePath); err != nil {
return err
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants