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
- Send a small, correctly URL-encoded form: curl -d 'node=machine-name' (under 4096 bytes) with Content-Type application/x-www-form-urlencoded.
- If the node identifier is short, use the GET variant instead: /debug/ping?node=<name>, which skips form parsing entirely.
- Verify no proxy in front of headscale is inflating or corrupting the request body.
- 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
- Send node as a small application/x-www-form-urlencoded body, never JSON, to this handler.
- Keep payloads well under the 4096-byte MaxBytesReader cap.
- Prefer /debug/ping?node=... GET links when scripting.
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
- health check timed out
- MOCKOIDC_CLIENT_ID not defined
- command aborted by user
- --name or --identifier flag is required
- HEADSCALE_CLI_API_KEY environment variable needs to be set
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/5cd2552ce2fd7d05.
Report an issue: GitHub.