grafana/k6 · error

uploading archive: %w

Error message

uploading archive: %w

What it means

After building the PUT, UploadArchive sends it through doWithRetry (internal/cloudapi/provisioning/api.go:103-107), which retries transport errors and 5xx responses and then gives up. This wrapped error means the request failed at the transport level or kept getting 5xx from the object storage - the presigned URL endpoint was not successfully reached within the retry budget. Auth travels in the URL query string, so intermediaries that alter the URL are a prime suspect.

Source

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

	PushPeriodSeconds string
	MessageMaxSize    int32
	AllowedLabels     []string
}

// UploadArchive PUTs pre-serialised archive bytes to the given
// presigned S3 URL. The URL carries auth in query params, so no
// Authorization header is set. Retries on 5xx and transport errors.
func (c *Client) UploadArchive(ctx context.Context, uploadURL string, body []byte) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadURL, bytes.NewReader(body))
	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
}

View on GitHub (pinned to 93accf6570)

Solutions

  1. Test raw reachability from the same environment: 'curl -v --upload-file archive.tar "<presigned-url>"' and compare failures
  2. Add the object-storage/upload domain to the egress allowlist (it differs from the API host)
  3. Ensure proxies do not rewrite or truncate the URL query string - presigned auth breaks if any param changes
  4. Retry the run: presigned URLs expire, and a fresh run gets a fresh URL

Example fix

# before - CI allows only the API host
allow: [api.cloud.k6.io:443]

# after - also allow the archive upload host printed in the URL
allow: [api.cloud.k6.io:443, *.storage.googleapis.com:443]
Defensive patterns

Strategy: retry

Validate before calling

# verify the storage host is reachable before the run
curl -sS -o /dev/null -w '%{http_code}\n' "$(echo "$ARCHIVE_UPLOAD_URL" | cut -d? -f1)" || echo 'upload host unreachable from this environment'

Try / catch

if err := client.UploadArchive(ctx, url, body); err != nil {
    if strings.Contains(err.Error(), "uploading archive") {
        // transport/5xx failure after retries: retry the whole provisioning -
        // a new run gets a fresh presigned URL, which also sidesteps expiry
    }
}

Prevention

When it happens

Trigger: Egress firewall/proxy blocking the S3/object-storage domain; a proxy truncating the long presigned query string; DNS or TLS failures to the bucket host; connection resets or timeouts on large archive uploads; sustained 5xx from the storage service.

Common situations: CI runners with strict egress allowlists that only permit the API host and not the storage host; corporate proxies with URL length limits that strip signature parameters; slow links timing out while uploading multi-MB archives.

Related errors


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