{"record":{"id":"70c1a9a6734fcb80","repo":"tailscale/tailscale","slug":"bad-ts-dial-upgrade","errorCode":null,"errorMessage":"bad ts-dial upgrade","messagePattern":"bad ts-dial upgrade","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"ipn/localapi/localapi.go","lineNumber":1318,"sourceCode":"\t}\n\tres, err := h.b.Ping(ctx, ip, tailcfg.PingType(pingTypeStr), size)\n\tif err != nil {\n\t\tWriteErrorJSON(w, err)\n\t\treturn\n\t}\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tjson.NewEncoder(w).Encode(res)\n}\n\nfunc (h *Handler) serveDial(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != httpm.POST {\n\t\thttp.Error(w, \"POST required\", http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\tconst upgradeProto = \"ts-dial\"\n\tif !strings.Contains(r.Header.Get(\"Connection\"), \"upgrade\") ||\n\t\tr.Header.Get(\"Upgrade\") != upgradeProto {\n\t\thttp.Error(w, \"bad ts-dial upgrade\", http.StatusBadRequest)\n\t\treturn\n\t}\n\thostStr, portStr := r.Header.Get(\"Dial-Host\"), r.Header.Get(\"Dial-Port\")\n\tif hostStr == \"\" || portStr == \"\" {\n\t\thttp.Error(w, \"missing Dial-Host or Dial-Port header\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tnetwork := cmp.Or(r.Header.Get(\"Dial-Network\"), \"tcp\")\n\n\taddr := net.JoinHostPort(hostStr, portStr)\n\n\t// Check whether the resolved address is a Tailscale route.\n\t// If not, tell the client to dial it directly so the connection\n\t// comes from the calling user's UID rather than our root-owned daemon.\n\tipp, viaTailscale, err := h.b.Dialer().UserDialPlan(r.Context(), network, addr)\n\tif err != nil {\n\t\thttp.Error(w, \"resolve failure: \"+err.Error(), http.StatusBadGateway)\n\t\treturn","sourceCodeStart":1300,"sourceCodeEnd":1336,"githubUrl":"https://github.com/tailscale/tailscale/blob/6e0912f97994f927632b34ae9e63b53d6516a6ac/ipn/localapi/localapi.go#L1300-L1336","documentation":"Returned as HTTP 400 by serveDial when the request is a POST but lacks the Connection: upgrade and Upgrade: ts-dial headers. The endpoint hijacks the connection into a raw TCP stream and refuses non-upgrade requests.","triggerScenarios":"Thrown at ipn/localapi/localapi.go:1318 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":["Send Connection: upgrade and Upgrade: ts-dial headers","Use the tailscale CLI/library dial support that speaks the protocol","Verify intermediaries preserve the headers"],"exampleFix":null,"handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"6e0912f97994f927632b34ae9e63b53d6516a6ac","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}