{"record":{"id":"45d80c8940127bfd","repo":"grafana/k6","slug":"creating-upload-request-w","errorCode":null,"errorMessage":"creating upload request: %w","messagePattern":"creating upload request: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/cloudapi/provisioning/api.go","lineNumber":100,"sourceCode":"// LogsConfig holds the log-push configuration from the provisioning\n// API's runtime_config.logs object. tail_url is intentionally omitted:\n// --local-execution pushes logs, it doesn't tail them.\ntype LogsConfig struct {\n\tPushURL           string\n\tLevel             string\n\tLimit             int32\n\tPushPeriodSeconds string\n\tMessageMaxSize    int32\n\tAllowedLabels     []string\n}\n\n// UploadArchive PUTs pre-serialised archive bytes to the given\n// presigned S3 URL. The URL carries auth in query params, so no\n// Authorization header is set. Retries on 5xx and transport errors.\nfunc (c *Client) UploadArchive(ctx context.Context, uploadURL string, body []byte) error {\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadURL, bytes.NewReader(body))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating upload request: %w\", err)\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/x-tar\")\n\treq.ContentLength = int64(len(body))\n\n\tresp, err := c.doWithRetry(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"uploading archive: %w\", err)\n\t}\n\tdefer func() {\n\t\t_, _ = io.Copy(io.Discard, resp.Body)\n\t\t_ = resp.Body.Close()\n\t}()\n\n\tif resp.StatusCode < 200 || resp.StatusCode >= 300 {\n\t\trespBody, readErr := io.ReadAll(resp.Body)\n\t\tif readErr != nil || len(respBody) == 0 {\n\t\t\treturn fmt.Errorf(\"archive upload failed: %d %s\",\n\t\t\t\tresp.StatusCode, http.StatusText(resp.StatusCode))","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/grafana/k6/blob/93accf6570dcd306ca5e99cc44c393ee3797761b/internal/cloudapi/provisioning/api.go#L82-L118","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before - wrapped line in .env silently adds a newline\nK6_ARCHIVE_UPLOAD_URL=https://bucket.s3.amazonaws.com/...\n?X-Amz-Signature=abc123\n\n# after - single line, quoted\nK6_ARCHIVE_UPLOAD_URL='https://bucket.s3.amazonaws.com/...?X-Amz-Signature=abc123'","handlingStrategy":"validation","validationCode":"# orchestrators: validate the upload URL is absolute before invoking k6\npython3 - <<'EOF'\nfrom urllib.parse import urlparse\nimport os, sys\nu = os.environ.get('ARCHIVE_UPLOAD_URL', '')\np = urlparse(u)\nassert p.scheme in ('http', 'https') and p.netloc, f'not an absolute URL: {u!r}'\nassert '\\n' not in u and ' ' not in u, 'upload URL contains whitespace'\nEOF","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["cloud","url","archive","upload","configuration","provisioning"],"backgroundTag":null,"analyzedSha":"93accf6570dcd306ca5e99cc44c393ee3797761b","analyzedAt":"2026-08-15T21:23:27.118Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}