grafana/k6 · error

archive upload failed: %d %s

Error message

archive upload failed: %d %s

What it means

The presigned PUT returned a non-2xx status and the response body was empty or unreadable, so UploadArchive can only report the numeric code and reason phrase (internal/cloudapi/provisioning/api.go:113-118). The upload was rejected by the storage service or something in front of it, but the error carries no server-side detail to narrow it down.

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 93accf6570)

Solutions

  1. Act on the status code: 403 usually means the presigned URL expired - re-run to get a fresh one; 400/405 suggests an intermediary mishandling the PUT
  2. Verify the runner's clock is NTP-synced - signature windows are time-sensitive
  3. Minimize the gap between start_local_execution and the upload (avoid long setup steps before upload)
  4. Capture the exchange with a debugging proxy to see who actually answered
Defensive patterns

Strategy: retry

Try / catch

if err := c.UploadArchive(ctx, url, body); err != nil {
    var status int
    if _, e := fmt.Sscanf(err.Error(), "archive upload failed: %d", &status); e == nil && status == 403 {
        // likely expired presigned URL: re-provision the run for a fresh one instead of reusing this URL
    }
}

Prevention

When it happens

Trigger: 403 with empty body from an expired presigned token (some providers and load balancers omit the XML); 400/405 from gateways that mishandle PUT or query-string auth; 5xx error pages with zero-length bodies.

Common situations: Delays between provisioning and upload (script setup, slow archive build) outliving the URL's validity; clock skew on the runner invalidating the signature window; middleboxes answering before the request reaches storage.

Related errors


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