grafana/k6 · error

archive upload failed

Error message

archive upload failed: %d %s

What it means

Returned by Client.UploadArchive (internal/cloudapi/provisioning/api.go:117) when the PUT to the presigned S3 URL completed at HTTP level but returned a non-2xx status whose body could not be read (readErr != nil) or was empty. You get the numeric status and status text only — no S3 XML detail. Because doWithRetry already retried 5xx, a non-2xx reaching this branch is typically a 4xx client-side rejection (auth/signature/routing) or an uncommon non-2xx with no body.

Solutions

  1. Map the status: 403 => expired or mismatched presigned signature (most common); 400 => malformed request/proxy rewrite; 5xx with empty body => intermediary stripping bodies.
  2. For 403, re-run the command so a fresh presigned URL is minted; avoid long delays between StartLocalExecution and UploadArchive.
  3. Check system clock synchronization (chrony/ntpd) — signature validation is time-sensitive.
  4. If a proxy is in play, bypass it for the S3 host (NO_PROXY) or verify it does not alter headers/query.
  5. If bodies are being stripped, capture with --http-debug or a packet capture to recover the S3 error detail.

Example fix

// before
if err := c.UploadArchive(ctx, uploadURL, body); err != nil {
	return err // "archive upload failed: 403 Forbidden"
}

// after — translate the common status for the operator
if err := c.UploadArchive(ctx, uploadURL, body); err != nil {
	if strings.Contains(err.Error(), "403") {
		return fmt.Errorf("archive upload rejected (presigned URL expired or signature mismatch), re-run to mint a new URL: %w", err)
	}
	return err
}
Defensive patterns

Strategy: retry

Try / catch

if err := c.UploadArchive(ctx, uploadURL, body); err != nil {
	msg := err.Error()
	switch {
	case strings.Contains(msg, "403"):
		// expired/mismatched presigned signature → re-run to mint a new URL
	case strings.Contains(msg, "400"):
		// request altered in transit (proxy) → inspect NO_PROXY / header rewriting
	default:
		// surface as-is; empty-body non-2xx is rare enough to investigate manually
	}
}

Prevention

When it happens

Trigger: S3 (or an intermediary) answering the PUT with e.g. 403 whose body is empty or the connection drops while reading the error body; a proxy returning a bare 400/502 with zero-length body; statuses outside 200-299 after retries finished.

Common situations: Presigned URL expired before the upload started (slow start, paused process, retry much later); system clock skew invalidating the signature; corporate proxy rewriting the request so the signature no longer matches; bucket/policy changes making the URL invalid.

Related errors


AI-assisted analysis of grafana/k6@01ffac6f24 (2026-08-18). Data as JSON: /api/errors/9a3585d5c57d2849. Report an issue: GitHub.

Appendix: source

Thrown at internal/cloudapi/provisioning/api.go:117

	if err != nil {
		return fmt.Errorf("creating upload request: %w", err)
	}
	req.Header.Set("Content-Type", "application/x-tar")
	req.ContentLength = int64(len(body))

	resp, err := c.doWithRetry(req)
	if err != nil {
		return fmt.Errorf("uploading archive: %w", err)
	}
	defer func() {
		_, _ = io.Copy(io.Discard, resp.Body)
		_ = resp.Body.Close()
	}()

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		respBody, readErr := io.ReadAll(resp.Body)
		if readErr != nil || len(respBody) == 0 {
			return fmt.Errorf("archive upload failed: %d %s",
				resp.StatusCode, http.StatusText(resp.StatusCode))
		}
		return fmt.Errorf("archive upload failed: %d %s: %s",
			resp.StatusCode, http.StatusText(resp.StatusCode), respBody)
	}

	return nil
}

// WaitForTestRunReady polls GET /cloud/v6/test_runs/{id} until the
// backend status becomes "initializing" (signalling that k6 can begin
// local execution). Returns an error if the test run reaches "aborted"
// or "completed" first. Cancellable via context. Logs status transitions
// at Debug level once per change.
//
// If pollInterval is <= 0 the defaultWaitPollInterval is used.
func (c *Client) WaitForTestRunReady(ctx context.Context, testRunID int64, pollInterval time.Duration) error {
	if pollInterval <= 0 {

View on GitHub (pinned to 01ffac6f24)