nektos/act · warning

cache %v %q: already complete

Error message

cache %v %q: already complete

What it means

After fetching the Cache record for an upload PATCH, the handler rejects the request with HTTP 400 'cache %v %q: already complete' when cache.Complete is true. A cache entry is marked complete by the commit endpoint (POST /caches/:id) after all chunks are uploaded; the upload endpoint refuses to write more data into a finalized cache to prevent corruption.

Source

Thrown at pkg/artifactcache/handler.go:273

	cache := &Cache{}
	db, err := h.openDB()
	if err != nil {
		h.responseJSON(w, r, 500, err)
		return
	}
	defer db.Close()
	if err := db.Get(id, cache); err != nil {
		if errors.Is(err, bolthold.ErrNotFound) {
			h.responseJSON(w, r, 400, fmt.Errorf("cache %d: not reserved", id))
			return
		}
		h.responseJSON(w, r, 500, err)
		return
	}

	if cache.Complete {
		h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
		return
	}
	db.Close()
	start, _, err := parseContentRange(r.Header.Get("Content-Range"))
	if err != nil {
		h.responseJSON(w, r, 400, err)
		return
	}
	if err := h.storage.Write(cache.ID, start, r.Body); err != nil {
		h.responseJSON(w, r, 500, err)
	}
	h.useCache(id)
	h.responseJSON(w, r, 200)
}

// POST /_apis/artifactcache/caches/:id
func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
	id, err := strconv.ParseInt(params.ByName("id"), 10, 64)

View on GitHub (pinned to 4f41128141)

Solutions

  1. Ensure upload order: reserve -> all PATCH uploads -> single commit; never PATCH after POST commit.
  2. If extra data must be stored, reserve a new cache (new key or version suffix) and upload to that ID.
  3. Make client retries idempotent: check the cache state (or treat 400 'already complete' as success for duplicate final chunks).
  4. For parallel writers, serialize uploads per cache ID or give each writer its own key.

Example fix

# before
curl -X POST  .../caches/43            # commit
curl -X PATCH .../caches/43 ...        # late chunk
# -> 400 cache 43 "linux-a": already complete

# after
curl -X PATCH .../caches/43 ...        # all chunks first
curl -X POST  .../caches/43            # commit last
Defensive patterns

Strategy: validation

Validate before calling

// Client-side ordering guard before uploading a chunk
func uploadChunk(id string, chunk []byte, committed bool) error {
    if committed {
        return fmt.Errorf("cache %d already committed; reserve a new cache to store more data", id)
    }
    return doPatch(id, chunk)
}

Try / catch

resp := uploadChunkViaPatch(id, chunk)
if resp.StatusCode == 400 && strings.Contains(resp.Body, "already complete") {
    // duplicate/late chunk after commit: treat as no-op success, or reserve a new cache
    return nil
}

Prevention

When it happens

Trigger: Sending additional PATCH /caches/:id uploads after the commit (POST) for that ID already succeeded: duplicated/retried upload requests, a client bug re-uploading after commit, or scripts that commit early and then continue chunk uploads.

Common situations: actions/cache retry logic firing after a slow commit; scripts driving the cache API manually with wrong ordering; parallel jobs sharing one cache ID and committing while another still uploads; network retries duplicating queued chunks after finalization.

Related errors


AI-assisted analysis of nektos/act@4f41128141 (2026-08-15). Data as JSON: /api/errors/f28e2143c04eb804. Report an issue: GitHub.