juanfont/headscale · warning

empty auth key in response

Error message

empty auth key in response

What it means

Returned as HTTP 400 by the POST branch of the /debug/ping handler when r.ParseForm() fails after the body has been wrapped in a MaxBytesReader limited to 4096 bytes. ParseForm fails on malformed URL-encoded bodies, invalid Content-Type form encodings, or when the body exceeds the 4 KiB cap (MaxBytesReader then makes ParseForm return an error).

Source

Thrown at cmd/dev/main.go:29

	"log"
	"net/http"
	"os"
	"os/exec"
	"os/signal"
	"path/filepath"
	"strconv"
	"syscall"
	"time"
)

var (
	port = flag.Int("port", 8080, "headscale listen port")
	keep = flag.Bool("keep", false, "keep state directory on exit")
)

var errHealthTimeout = errors.New("health check timed out")

var errEmptyAuthKey = errors.New("empty auth key in response")

// maxDevPort is the highest --port value that keeps the derived metrics
// port (port+1010) inside the valid 1..65535 TCP range.
const maxDevPort = 64525

const devConfig = `---
server_url: http://127.0.0.1:%d
listen_addr: 127.0.0.1:%d
metrics_listen_addr: 127.0.0.1:%d

noise:
  private_key_path: %s/noise_private.key

prefixes:
  v4: 100.64.0.0/10
  v6: fd7a:115c:a1e0::/48
  allocation: sequential

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Send a small, correctly URL-encoded form: curl -d 'node=machine-name' (under 4096 bytes) with Content-Type application/x-www-form-urlencoded.
  2. If the node identifier is short, use the GET variant instead: /debug/ping?node=<name>, which skips form parsing entirely.
  3. Verify no proxy in front of headscale is inflating or corrupting the request body.
  4. Check for accidental multipart or JSON bodies; this handler only reads r.FormValue("node").

Example fix

# before (400 bad form data)
curl -X POST http://host/debug/ping -H 'Content-Type: application/json' -d '{"node":"big-machine"}'

# after
curl -X POST http://host/debug/ping -d 'node=big-machine'
# or simply
curl 'http://host/debug/ping?node=big-machine'
Defensive patterns

Strategy: validation

Validate before calling

// Prefer the GET form; if POSTing, keep the body a tiny url-encoded form.
if len(nodeQuery) > 4000 { // handler caps at 4096
    return fmt.Errorf("node query too large for /debug/ping")
}
values := url.Values{"node": {nodeQuery}}
req, _ := http.NewRequest(http.MethodPost, debugURL+"/ping", strings.NewReader(values.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

Try / catch

resp, err := http.DefaultClient.Do(req)
if err == nil && resp.StatusCode == http.StatusBadRequest {
    // fall back to the GET variant which never parses a form
    resp2, _ := http.Get(debugURL + "/ping?node=" + url.QueryEscape(nodeQuery))
    defer resp2.Body.Close()
}

Prevention

When it happens

Trigger: POSTing to /debug/ping with a body larger than 4096 bytes; sending a form with broken URL-encoding (e.g. stray % sequences); POSTing with Content-Type application/x-www-form-urlencoded but non-form payloads such as raw JSON; truncated/chunked request bodies.

Common situations: Scripts that POST JSON instead of form fields; pasting a huge node list or long node FQDN into the ping form; a proxy that mangles or truncates the body; automated clients that omit Content-Type.

Related errors


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/5cd2552ce2fd7d05. Report an issue: GitHub.