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
- Do not use 'caddy upgrade' on development builds — rebuild with xcaddy including the local replacement instead
- Remove the replace directive and depend on a published version of the module, then upgrade normally
- 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
- Never run 'caddy upgrade' on xcaddy development builds using local --with paths
- Publish modules to a versioned source instead of replace directives for production builds
- Standardize on xcaddy build --with ... in CI for binaries with replaces
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
- unable to enumerate installed plugins: %v
- download failed: HTTP %d: %s (id=%s)
- --config is required
- provisioning admin router module %s: %v
- encoding new config: %v
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/c92eeff6a21e18c6.
Report an issue: GitHub.