{"record":{"id":"4e1e4b5beca1a1d3","repo":"tailscale/tailscale","slug":"failed-to-construct-request-s","errorCode":null,"errorMessage":"failed to construct request: %s","messagePattern":"failed to construct request: (.+?)","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"kube/metrics/metrics.go","lineNumber":29,"sourceCode":"\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"tailscale.com/client/local\"\n\t\"tailscale.com/client/tailscale/apitype\"\n)\n\n// metrics is a simple metrics HTTP server, if enabled it forwards requests to\n// the tailscaled's LocalAPI usermetrics endpoint at /localapi/v0/usermetrics.\ntype metrics struct {\n\tdebugEndpoint string\n\tlc            *local.Client\n}\n\nfunc proxy(w http.ResponseWriter, r *http.Request, url string, do func(*http.Request) (*http.Response, error)) {\n\treq, err := http.NewRequestWithContext(r.Context(), r.Method, url, r.Body)\n\tif err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"failed to construct request: %s\", err), http.StatusInternalServerError)\n\t\treturn\n\t}\n\treq.Header = r.Header.Clone()\n\n\tresp, err := do(req)\n\tif err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"failed to proxy request: %s\", err), http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\tfor key, val := range resp.Header {\n\t\tfor _, v := range val {\n\t\t\tw.Header().Add(key, v)\n\t\t}\n\t}\n\tw.WriteHeader(resp.StatusCode)\n\tif _, err := io.Copy(w, resp.Body); err != nil {","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/tailscale/tailscale/blob/6e0912f97994f927632b34ae9e63b53d6516a6ac/kube/metrics/metrics.go#L11-L47","documentation":"The kube metrics server proxies incoming requests to tailscaled's LocalAPI usermetrics endpoint by first rebuilding the request with http.NewRequestWithContext(r.Context(), r.Method, url, r.Body). This constructor validates the method token and URL, so a malformed method (illegal characters, empty) or invalid target URL yields this 500 'failed to construct request'. It is a client-protocol error surfaced by the proxy, not a metrics failure.","triggerScenarios":"A client issuing an HTTP request with an invalid method token (non-token bytes) that still reached the handler (e.g. via a frontend that relaxes validation), or an unusable target URL — in practice nearly impossible with the fixed http://LocalAPIHost/... URL unless the library is embedded and url/debugEndpoint are overridden with a malformed value.","commonSituations":"Embedding kube/metrics with a custom debug endpoint string that is not a valid absolute URL (missing scheme/host, control characters); exotic HTTP clients sending custom verbs with spaces.","solutions":["Send a normal, valid HTTP method (GET/POST) to the metrics endpoints","If embedding this package, validate the configured debugEndpoint is a valid host:port before serving","Reproduce with curl to confirm the raw request line is well-formed"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Embedders: validate config before serving\nif _, err := url.Parse(\"http://\" + debugEndpoint + \"/\"); debugEndpoint != \"\" && err != nil {\n    return fmt.Errorf(\"invalid debug endpoint %q: %w\", debugEndpoint, err)\n}","typeGuard":null,"tryCatchPattern":"if resp.StatusCode == http.StatusInternalServerError && strings.Contains(body, \"failed to construct request\") {\n    // malformed method/URL — fix the client's request line; retrying the same bytes will fail identically\n}","preventionTips":["Use standard HTTP verbs when scraping the metrics endpoints","Validate any custom debugEndpoint/host:port at startup, not per request"],"tags":["tailscale","kubernetes","metrics","http-proxy","request-validation"],"backgroundTag":"invalid-http-request","analyzedSha":"6e0912f97994f927632b34ae9e63b53d6516a6ac","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}