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
- Locate the <Code>...</Code> value inside the trailing body text — it is the authoritative S3 reason; act on that code first.
- AccessDenied/RequestTimeoutExpired: restart the run to get a new presigned URL and upload promptly; minimize work between StartLocalExecution and UploadArchive.
- EntityTooLarge: shrink the archive (drop large files from the script directory) or use --no-archive-upload (ArchiveSize 0 path) if scripts are self-contained.
- AuthorizationQueryParametersError: ensure nothing (proxy, wrapper, logging) rewrites the URL query; pass uploadURL byte-for-byte.
- 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
- Keep archives well under 2 GiB and free of large binary assets.
- Verify nothing rewrites the URL query (signatures cover query params).
- On slow links, prefer smaller archives so uploads finish inside the presigned window.
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)