GoogleContainerTools/skaffold · error
URI scheme is %q, must be 'gs'
Error message
URI scheme is %q, must be 'gs'
What it means
parseGCSURI rejects URIs whose scheme is not 'gs'. Both DownloadRecursive (src) and UploadFile (dst) require Google Cloud Storage URIs of the form gs://bucket/path. This error means a non-GCS location (local path, s3://, https://, or a bare path that yields an empty scheme) was passed.
Source
Thrown at pkg/skaffold/gcs/client/native.go:139
dstObj := urinfo.ObjPath
if isDirectory {
dstObj, err = url.JoinPath(dstObj, filepath.Base(src))
if err != nil {
return err
}
}
return bucket.UploadObject(ctx, dstObj, f)
}
func (n *Native) parseGCSURI(uri string) (uriInfo, error) {
var gcsobj uriInfo
u, err := url.Parse(uri)
if err != nil {
return uriInfo{}, fmt.Errorf("cannot parse URI %q: %w", uri, err)
}
if u.Scheme != "gs" {
return uriInfo{}, fmt.Errorf("URI scheme is %q, must be 'gs'", u.Scheme)
}
if u.Host == "" {
return uriInfo{}, errors.New("bucket name is empty")
}
gcsobj.Bucket = u.Host
// If we do this with the url package it will scape the `?` character, breaking the glob.
gcsobj.ObjPath = strings.TrimLeft(strings.ReplaceAll(uri, "gs://"+u.Host, ""), "/")
return gcsobj, nil
}
func (n *Native) filesToDownload(ctx context.Context, bucket bucketHandler, urinfo uriInfo) (map[string]string, error) {
uriToLocalPath := map[string]string{}
// The exact match is with the original glob expression. This could be:
// 1. a/b/c -> It will return the file `c` under a/b/ if it exists
// 2. a/b/c* -> It will return any file under a/b/ that starts with c, e.g, c1, c-other, etc
// 3. a/b/c** -> It will return any file that starts with 'c', and files inside any folder starting with 'c'. It is doing the recursion alreadyView on GitHub (pinned to a1189de023)
Solutions
- Prefix the argument with gs:// and a bucket name, e.g. "gs://my-bucket/path/**"
- Check the config for an empty/missing remote path and set it correctly
- Use the GCS URI from `gcloud storage ls` output rather than a console web URL
- Add an early check that strings.HasPrefix(arg, "gs://") in your calling code for a clearer failure
Example fix
// before
n.UploadFile(ctx, "./app.tar.gz", "artifacts/app.tar.gz") // no scheme
// after
if !strings.HasPrefix("artifacts/app.tar.gz", "gs://") {
return errors.New("destination must be a gs:// URI")
}
if err := n.UploadFile(ctx, "./app.tar.gz", "gs://artifacts/app.tar.gz"); err != nil { /* handle */ } Defensive patterns
Strategy: validation
Validate before calling
func requireGCSURI(uri string) error {
if !strings.HasPrefix(uri, "gs://") || len(strings.TrimPrefix(uri, "gs://")) == 0 {
return fmt.Errorf("expected gs://bucket/path, got %q", uri)
}
return nil
}
// call before DownloadRecursive/UploadFile Prevention
- Validate config values start with gs:// before invoking the library
- Don't confuse console https URLs with gs:// URIs
- Treat empty remote-path config as an error early in startup
- Use `gcloud storage ls` to copy canonical URIs
When it happens
Trigger: Calling DownloadRecursive with src like "/local/dir" or "s3://bucket/key", or UploadFile with a plain dst like "./remote-cache"; a bare hostname/path has empty scheme so u.Scheme != "gs" triggers this error.
Common situations: Config file mixes local and remote sources and the remote one is unset/empty; user pasted an https console URL instead of a gs:// URI; migrating from another tool that accepted s3-style URIs.
Related errors
- cannot parse URI %q: %w
- bucket name is empty
- %v is not a valid GCS path
- cannot add an empty image value
- CONFIG_REMOTE_REPO_CACHE_NOT_FOUND_ERR
AI-assisted analysis of GoogleContainerTools/skaffold@a1189de023 (2026-09-05).
Data as JSON: /api/errors/93623ae5cdcf46bf.
Report an issue: GitHub.