tailscale/tailscale · error
%s: %s
Error message
%s: %s
What it means
Returned by Client.PushFile (client/local/local.go:877) when PUTting a file to /localapi/v0/files/<target> does not return 200. The status and full body are combined into an error passed through bestError(), which prefers a structured message from the body when one exists. A 200 with an error body is handled elsewhere (bestError path after copying), so this line is strictly the non-OK status case.
Source
Thrown at client/local/local.go:877
// The name parameter is the original filename, not escaped.
func (lc *Client) PushFile(ctx context.Context, target tailcfg.StableNodeID, size int64, name string, r io.Reader) error {
req, err := http.NewRequestWithContext(ctx, "PUT", "http://"+apitype.LocalAPIHost+"/localapi/v0/file-put/"+string(target)+"/"+url.PathEscape(name), r)
if err != nil {
return err
}
if size != -1 {
req.ContentLength = size
}
res, err := lc.doLocalRequestNiceError(req)
if err != nil {
return err
}
if res.StatusCode == 200 {
io.Copy(io.Discard, res.Body)
return nil
}
all, _ := io.ReadAll(res.Body)
return bestError(fmt.Errorf("%s: %s", res.Status, all), all)
}
// CheckIPForwarding asks the local Tailscale daemon whether it looks like the
// machine is properly configured to forward IP packets as a subnet router
// or exit node.
//
// API maturity: this method is not considered a stable API and is
// subject to change between releases.
func (lc *Client) CheckIPForwarding(ctx context.Context) error {
if !buildfeatures.HasAdvertiseRoutes {
return nil
}
body, err := lc.get200(ctx, "/localapi/v0/check-ip-forwarding")
if err != nil {
return err
}
var jres struct {
Warning stringView on GitHub (pinned to cfe32b8be6)
Solutions
- Read the body in the error — it states whether it is policy, target validity, or state
- Verify the target with FileTargets and confirm it is online and file-capable
- Enable Taildrop/file receiving for the target (admin policy + node settings) before pushing
- Retry once after re-fetching targets if the failure looks transient
Defensive patterns
Strategy: validation
Validate before calling
// Confirm the target accepts files before pushing.
fts, err := lc.FileTargets(ctx)
if err != nil {
return err
}
ok := false
for _, ft := range fts {
if ft.Node.Key == targetKey {
ok = true
break
}
}
if !ok {
return fmt.Errorf("target %x not file-capable or offline", targetKey)
} Try / catch
if err := lc.PushFile(ctx, target, size, r); err != nil {
if strings.Contains(err.Error(), "not enabled") || strings.Contains(err.Error(), "denied") {
return errors.New("Taildrop disabled by policy or target settings")
}
return err // bestError already prefers the structured daemon message
} Prevention
- Check FileTargets right before the push, not from a cached snapshot
- Verify Taildrop is permitted by tailnet policy and enabled on the target device
- Surface the daemon's body text to users — it distinguishes policy vs. state failures
When it happens
Trigger: Pushing to a target that is not a valid peer (unknown key/IP); Taildrop file sending disabled by policy on the tailnet or for the target node; the target node offline or not accepting files; permission denial on the local API.
Common situations: Sending files to devices that have file receiving off; policy changes disabling Taildrop mid-integration; stale FileTargets results used after a peer went away.
Related errors
- unexpected chunking
- HTTP %s: %s
- unexpected output: no delimiter
- both GatewayAddr and SelfAddr must be provided if one is
- HTTP %s: %s
AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15).
Data as JSON: /api/errors/a04ca8106307f370.
Report an issue: GitHub.