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
- Map the status: 403 => expired or mismatched presigned signature (most common); 400 => malformed request/proxy rewrite; 5xx with empty body => intermediary stripping bodies.
- For 403, re-run the command so a fresh presigned URL is minted; avoid long delays between StartLocalExecution and UploadArchive.
- Check system clock synchronization (chrony/ntpd) — signature validation is time-sensitive.
- If a proxy is in play, bypass it for the S3 host (NO_PROXY) or verify it does not alter headers/query.
- 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
- Keep system clocks synchronized (NTP) so presigned-URL signature windows validate.
- Start the upload promptly after provisioning; avoid sleeping between StartLocalExecution and UploadArchive.
- Bypass HTTP proxies for the S3 upload host so neither URL nor headers get rewritten.
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)