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
- 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
- Verify the runner's clock is NTP-synced - signature windows are time-sensitive
- Minimize the gap between start_local_execution and the upload (avoid long setup steps before upload)
- 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
- Start the upload promptly after provisioning - presigned URLs have short lifetimes
- Keep runner clocks NTP-synced; skewed clocks invalidate time-window signatures
- Log the numeric status when it occurs - 403 vs 400 vs 5xx points to expiry vs rewriting vs outage
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
- archive upload failed: %d %s: %s
- creating upload request: %w
- uploading archive: %w
- fetching test status: %w
- upload archive: %w
AI-assisted analysis of grafana/k6@93accf6570 (2026-08-15).
Data as JSON: /api/errors/9a3585d5c57d2849.
Report an issue: GitHub.