caddyserver/caddy · error

cannot auto-upgrade when Go module has been replaced: %s =>

Error message

cannot auto-upgrade when Go module has been replaced: %s => %s

What it means

getPluginPackages() refuses to proceed when any compiled-in plugin's Go module has a Replace directive in the go.mod used to build the binary. The build API constructs binaries from published module versions, so a locally-replaced dependency cannot be reproduced server-side; auto-upgrade would silently drop the replacement.

Source

Thrown at cmd/packagesfuncs.go:311

			Error      struct {
				Message string `json:"message"`
				ID      string `json:"id"`
			} `json:"error"`
		}
		err2 := json.NewDecoder(resp.Body).Decode(&details)
		if err2 != nil {
			return nil, fmt.Errorf("download and error decoding failed: HTTP %d: %v", resp.StatusCode, err2)
		}
		return nil, fmt.Errorf("download failed: HTTP %d: %s (id=%s)", resp.StatusCode, details.Error.Message, details.Error.ID)
	}
	return resp, nil
}

func getPluginPackages(modules []moduleInfo) (map[string]pluginPackage, error) {
	pluginPkgs := make(map[string]pluginPackage)
	for _, mod := range modules {
		if mod.goModule.Replace != nil {
			return nil, fmt.Errorf("cannot auto-upgrade when Go module has been replaced: %s => %s",
				mod.goModule.Path, mod.goModule.Replace.Path)
		}
		pluginPkgs[mod.goModule.Path] = pluginPackage{Version: mod.goModule.Version, Path: mod.goModule.Path}
	}
	return pluginPkgs, nil
}

func writeCaddyBinary(path string, body *io.ReadCloser, fileInfo os.FileInfo) error {
	l := caddy.Log()
	destFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileInfo.Mode())
	if err != nil {
		return fmt.Errorf("unable to open destination file: %v", err)
	}
	defer destFile.Close()

	l.Info("downloading binary", zap.String("destination", path))

	_, err = io.Copy(destFile, *body)

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Do not use 'caddy upgrade' on development builds — rebuild with xcaddy including the local replacement instead
  2. Remove the replace directive and depend on a published version of the module, then upgrade normally
  3. If the replacement is permanent, treat the binary as a custom build and manage upgrades through your own build pipeline (xcaddy build --with ...)

Example fix

# before
# go.mod of your custom build:
#   require github.com/foo/bar v0.0.0
#   replace github.com/foo/bar => ../bar
caddy upgrade  # error: cannot auto-upgrade when Go module has been replaced

# after
xcaddy build --with github.com/foo/bar=../bar   # rebuild with the replacement explicitly
Defensive patterns

Strategy: validation

Validate before calling

go version -m "$(command -v caddy)" | grep -i '=> ' && echo 'has replace directives: do NOT auto-upgrade; rebuild with xcaddy'

Prevention

When it happens

Trigger: Building caddy with xcaddy from a checkout whose go.mod (or a dependency's go.mod) contains 'replace' directives — e.g. developing a plugin locally with 'replace github.com/foo/bar => ../bar', or using a fork pinned via replace.

Common situations: Plugin developers testing a local build with xcaddy --with github.com/me/myplugin=/path/to/local (which adds a replace), then running 'caddy upgrade' on that binary; teams pinning forks of dependencies via replace in a custom build.

Related errors


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/c92eeff6a21e18c6. Report an issue: GitHub.