{"record":{"id":"b3727310e0f5297d","repo":"juanfont/headscale","slug":"bad-request-invalid-json","errorCode":null,"errorMessage":"Bad Request: invalid JSON","messagePattern":"Bad Request: invalid JSON","errorType":"http","errorClass":"HTTPError","httpStatus":400,"severity":"warning","filePath":"hscontrol/handlers.go","lineNumber":137,"sourceCode":"\n// verifyBodyLimit caps the request body for /verify. The DERP verify\n// protocol payload ([tailcfg.DERPAdmitClientRequest]) is a few hundred\n// bytes; 4 KiB is generous and prevents an unauthenticated client from\n// OOMing the public router with arbitrarily large POSTs.\nconst verifyBodyLimit int64 = 4 * 1024\n\nfunc (h *Headscale) handleVerifyRequest(\n\treq *http.Request,\n\twriter io.Writer,\n) error {\n\tbody, err := io.ReadAll(req.Body)\n\tif err != nil {\n\t\treturn NewHTTPError(http.StatusRequestEntityTooLarge, \"request body too large\", fmt.Errorf(\"reading request body: %w\", err))\n\t}\n\n\tvar derpAdmitClientRequest tailcfg.DERPAdmitClientRequest\n\tif err := json.Unmarshal(body, &derpAdmitClientRequest); err != nil { //nolint:noinlineerr\n\t\treturn NewHTTPError(http.StatusBadRequest, \"Bad Request: invalid JSON\", fmt.Errorf(\"parsing DERP client request: %w\", err))\n\t}\n\n\tallow := h.state.ListNodes().ContainsFunc(func(n types.NodeView) bool {\n\t\treturn n.NodeKey() == derpAdmitClientRequest.NodePublic\n\t})\n\n\tresp := &tailcfg.DERPAdmitClientResponse{\n\t\tAllow: allow,\n\t}\n\n\treturn json.NewEncoder(writer).Encode(resp)\n}\n\n// VerifyHandler see https://github.com/tailscale/tailscale/blob/964282d34f06ecc06ce644769c66b0b31d118340/derp/derp_server.go#L1159\n// DERP use verifyClientsURL to verify whether a client is allowed to connect to the DERP server.\nfunc (h *Headscale) VerifyHandler(\n\twriter http.ResponseWriter,\n\treq *http.Request,","sourceCodeStart":119,"sourceCodeEnd":155,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/handlers.go#L119-L155","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\ncurl -X POST http://headscale/verify -d 'not json'\n\n// after\ncurl -X POST http://headscale/verify -H 'Content-Type: application/json' -d '{\"NodePublic\":\"nodekey:abcdef...\"}'","handlingStrategy":"validation","validationCode":"if !json.Valid(body) {\n    return fmt.Errorf(\"verify body is not valid JSON\")\n}\nvar r tailcfg.DERPAdmitClientRequest\nif err := json.Unmarshal(body, &r); err != nil || r.NodePublic.IsZero() {\n    return fmt.Errorf(\"body is not a DERPAdmitClientRequest\")\n}","typeGuard":null,"tryCatchPattern":"if err := json.Unmarshal(body, &req); err != nil {\n    return fmt.Errorf(\"parsing DERP client request: %w\", err)\n}","preventionTips":["Send Content-Type: application/json and a fully marshalled struct","Log the raw body on 400 to spot proxy-injected error pages"],"tags":["derp","json","http","validation"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}