grafana/k6 · error
creating upload request: %w
Error message
creating upload request: %w
What it means
UploadArchive (internal/cloudapi/provisioning/api.go:97-101) starts by building an http PUT request to the presigned upload URL the provisioning backend returned. http.NewRequestWithContext fails only for a malformed URL (control characters, invalid port, or a URL that cannot be parsed at all), so this error means the archive upload URL itself is not a valid absolute URL - nothing was sent over the network yet.
Source
Thrown at internal/cloudapi/provisioning/api.go:100
// LogsConfig holds the log-push configuration from the provisioning
// API's runtime_config.logs object. tail_url is intentionally omitted:
// --local-execution pushes logs, it doesn't tail them.
type LogsConfig struct {
PushURL string
Level string
Limit int32
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))View on GitHub (pinned to 93accf6570)
Solutions
- Print/inspect the exact upload URL the run received (enable debug logging) and compare it with what the backend issued
- Fix the provisioning service or mock to return a full absolute https URL including query parameters
- Sanitize env values in CI (strip quotes, join wrapped lines) before launching k6
Example fix
# before - wrapped line in .env silently adds a newline K6_ARCHIVE_UPLOAD_URL=https://bucket.s3.amazonaws.com/... ?X-Amz-Signature=abc123 # after - single line, quoted K6_ARCHIVE_UPLOAD_URL='https://bucket.s3.amazonaws.com/...?X-Amz-Signature=abc123'
Defensive patterns
Strategy: validation
Validate before calling
# orchestrators: validate the upload URL is absolute before invoking k6
python3 - <<'EOF'
from urllib.parse import urlparse
import os, sys
u = os.environ.get('ARCHIVE_UPLOAD_URL', '')
p = urlparse(u)
assert p.scheme in ('http', 'https') and p.netloc, f'not an absolute URL: {u!r}'
assert '\n' not in u and ' ' not in u, 'upload URL contains whitespace'
EOF Prevention
- Keep presigned URLs on a single line in env files and secrets - newlines break them silently
- Backend contract tests should assert ArchiveUploadURL starts with https:// and carries query params
- Never template-substitute upload URLs by hand; pass through what the API returned
When it happens
Trigger: ArchiveUploadURL from the start_local_execution response (or the orchestrator injecting it) being a relative path, a template placeholder that was never filled, a value with embedded newlines/spaces from CI env mangling, or a truncated copy-paste.
Common situations: Backend or mock returning '/upload/abc' instead of 'https://bucket.../upload?X-Amz-Signature=...'; secrets/CI systems wrapping values in quotes that end up literal; line-wrapped env files inserting newlines into long presigned URLs.
Related errors
- archive upload failed: %d %s
- archive upload failed: %d %s: %s
- missing presigned url in response body
- couldn't parse cloud logs host %w
- uploading archive: %w
AI-assisted analysis of grafana/k6@93accf6570 (2026-08-15).
Data as JSON: /api/errors/45d80c8940127bfd.
Report an issue: GitHub.