grafana/k6 · error

archive upload failed

Error message

archive upload failed: %d %s: %s

What it means

Returned by Client.UploadArchive (internal/cloudapi/provisioning/api.go:120) when the PUT to the presigned S3 URL returned a non-2xx status and the response body was successfully read — the body is appended verbatim, which for S3 is an XML <Error> document naming the exact S3 code (e.g. AccessDenied, RequestTimeoutExpired, EntityTooLarge). This is the most informative of the three upload-failure variants; read the S3 Code element to know precisely why the object write was refused.

Solutions

  1. Locate the <Code>...</Code> value inside the trailing body text — it is the authoritative S3 reason; act on that code first.
  2. AccessDenied/RequestTimeoutExpired: restart the run to get a new presigned URL and upload promptly; minimize work between StartLocalExecution and UploadArchive.
  3. EntityTooLarge: shrink the archive (drop large files from the script directory) or use --no-archive-upload (ArchiveSize 0 path) if scripts are self-contained.
  4. AuthorizationQueryParametersError: ensure nothing (proxy, wrapper, logging) rewrites the URL query; pass uploadURL byte-for-byte.
  5. Persist clock sync on the host to prevent signature window misses.

Example fix

// before
return fmt.Errorf("archive upload failed: %d %s: %s", resp.StatusCode, http.StatusText(resp.StatusCode), respBody)

// after — extract the S3 error code for actionable output
var s3code string
if m := regexp.MustCompile(`<Code>([^<]+)</Code>`).FindSubmatch(respBody); m != nil {
	s3code = string(m[1])
}
return fmt.Errorf("archive upload failed: %d %s (s3 code %q): %s", resp.StatusCode, http.StatusText(resp.StatusCode), s3code, respBody)
Defensive patterns

Strategy: retry

Try / catch

if err := c.UploadArchive(ctx, uploadURL, body); err != nil {
	if code := s3ErrorCode(err); code != "" {
		switch code {
		case "AccessDenied", "RequestTimeoutExpired":
			// re-run provisioning for a fresh presigned URL, then retry once
		case "EntityTooLarge":
			// shrink the archive or use --no-archive-upload; do not retry
		default:
			return err
		}
	}
}

func s3ErrorCode(err error) string {
	m := regexp.MustCompile(`<Code>([^<]+)</Code>`).FindStringSubmatch(err.Error())
	if m == nil {
		return ""
	}
	return m[1]
}

Prevention

When it happens

Trigger: PUT answered with 403 + <Error><Code>AccessDenied</Code> (expired/abused presigned URL), 400 + AuthorizationQueryParametersError (tampered query string), 413 + EntityTooLarge (object exceeds bucket/account limit), 408/400 + RequestTimeout (slow upload over the signed window).

Common situations: Delay between start_local_execution and the actual PUT exceeding the URL's signing window on slow archive builds; proxies or scripts appending/encoding extra query params; archives above the S3/bucket size policy; clock skew.

Related errors


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

Appendix: source

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

	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 {
		pollInterval = defaultWaitPollInterval
	}

View on GitHub (pinned to 01ffac6f24)