juanfont/headscale · warning · HTTPError
Bad Request: invalid JSON
Error message
Bad Request: invalid JSON
What it means
Returned by handleVerifyRequest when the /verify request body does not unmarshal into tailcfg.DERPAdmitClientRequest. The server expects a small JSON object with a NodePublic field; any other body (HTML error page, form data, truncated JSON) triggers this 400.
Source
Thrown at hscontrol/handlers.go:137
// verifyBodyLimit caps the request body for /verify. The DERP verify
// protocol payload ([tailcfg.DERPAdmitClientRequest]) is a few hundred
// bytes; 4 KiB is generous and prevents an unauthenticated client from
// OOMing the public router with arbitrarily large POSTs.
const verifyBodyLimit int64 = 4 * 1024
func (h *Headscale) handleVerifyRequest(
req *http.Request,
writer io.Writer,
) error {
body, err := io.ReadAll(req.Body)
if err != nil {
return NewHTTPError(http.StatusRequestEntityTooLarge, "request body too large", fmt.Errorf("reading request body: %w", err))
}
var derpAdmitClientRequest tailcfg.DERPAdmitClientRequest
if err := json.Unmarshal(body, &derpAdmitClientRequest); err != nil { //nolint:noinlineerr
return NewHTTPError(http.StatusBadRequest, "Bad Request: invalid JSON", fmt.Errorf("parsing DERP client request: %w", err))
}
allow := h.state.ListNodes().ContainsFunc(func(n types.NodeView) bool {
return n.NodeKey() == derpAdmitClientRequest.NodePublic
})
resp := &tailcfg.DERPAdmitClientResponse{
Allow: allow,
}
return json.NewEncoder(writer).Encode(resp)
}
// VerifyHandler see https://github.com/tailscale/tailscale/blob/964282d34f06ecc06ce644769c66b0b31d118340/derp/derp_server.go#L1159
// DERP use verifyClientsURL to verify whether a client is allowed to connect to the DERP server.
func (h *Headscale) VerifyHandler(
writer http.ResponseWriter,
req *http.Request,View on GitHub (pinned to 565fd254d0)
Solutions
- Inspect the exact body being sent to /verify and compare against tailcfg.DERPAdmitClientRequest
- Ensure no reverse proxy rewrites or replaces the request body
- Confirm the DERP router and headscale versions agree on the verify protocol
Example fix
// before
curl -X POST http://headscale/verify -d 'not json'
// after
curl -X POST http://headscale/verify -H 'Content-Type: application/json' -d '{"NodePublic":"nodekey:abcdef..."}' Defensive patterns
Strategy: validation
Validate before calling
if !json.Valid(body) {
return fmt.Errorf("verify body is not valid JSON")
}
var r tailcfg.DERPAdmitClientRequest
if err := json.Unmarshal(body, &r); err != nil || r.NodePublic.IsZero() {
return fmt.Errorf("body is not a DERPAdmitClientRequest")
} Try / catch
if err := json.Unmarshal(body, &req); err != nil {
return fmt.Errorf("parsing DERP client request: %w", err)
} Prevention
- Send Content-Type: application/json and a fully marshalled struct
- Log the raw body on 400 to spot proxy-injected error pages
When it happens
Trigger: POSTing non-JSON or malformed JSON to /verify, e.g. an empty body, a proxied HTML error page, or a JSON payload missing/mistyping the NodePublic field.
Common situations: Pointing a DERP verify client at the wrong URL; a reverse proxy intercepting the request and returning its own error body; version skew where the sender emits a different verify payload schema.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- HEADSCALE_CLI_API_KEY environment variable needs to be set
- invalid group member type
- group value must be an array of users
- hostname contains invalid IP address
- invalid ACL action
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/b3727310e0f5297d.
Report an issue: GitHub.