mirror of
https://github.com/tailscale/tailscale.git
synced 2026-06-24 16:01:30 -04:00
Several packages built their HTTP transports with
http.DefaultTransport.(*http.Transport).Clone()
The standard library only documents http.DefaultTransport as an
http.RoundTripper, so an application is free to replace it with a
RoundTripper that is not a *http.Transport (e.g. an instrumented or
tracing wrapper). When such an application embeds tsnet.Server, the
unchecked type assertion panics as soon as tsnet brings up its control
connection, DNS bootstrap, or log uploader.
Add netutil.NewDefaultTransport, which returns a clone of the global
when it is still the standard *http.Transport (preserving existing
behavior) and otherwise returns a fresh transport mirroring the stdlib
defaults. Route every clone site through it.
Updates #19937
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Achille Roussel <achille.roussel@gmail.com>
71 lines
1.7 KiB
Go
71 lines
1.7 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package prober
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
"tailscale.com/net/netutil"
|
|
)
|
|
|
|
const maxHTTPBody = 4 << 20 // MiB
|
|
|
|
// HTTP returns a ProbeClass that healthchecks an HTTP URL.
|
|
//
|
|
// The probe function sends a GET request for url, expects an HTTP 200
|
|
// response, and verifies that want is present in the response
|
|
// body.
|
|
func HTTP(url, wantText string) ProbeClass {
|
|
return ProbeClass{
|
|
Probe: func(ctx context.Context) error {
|
|
return probeHTTP(ctx, url, []byte(wantText))
|
|
},
|
|
Class: "http",
|
|
}
|
|
}
|
|
|
|
func probeHTTP(ctx context.Context, url string, want []byte) error {
|
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("constructing request: %w", err)
|
|
}
|
|
|
|
// Get a completely new transport each time, so we don't reuse a
|
|
// past connection.
|
|
tr := netutil.NewDefaultTransport()
|
|
defer tr.CloseIdleConnections()
|
|
c := &http.Client{
|
|
Transport: tr,
|
|
}
|
|
|
|
resp, err := c.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("fetching %q: %w", url, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != 200 {
|
|
return fmt.Errorf("fetching %q: status code %d, want 200", url, resp.StatusCode)
|
|
}
|
|
|
|
bs, err := io.ReadAll(io.LimitReader(resp.Body, maxHTTPBody))
|
|
if err != nil {
|
|
return fmt.Errorf("reading body of %q: %w", url, err)
|
|
}
|
|
|
|
if !bytes.Contains(bs, want) {
|
|
// Log response body, but truncate it if it's too large; the limit
|
|
// has been chosen arbitrarily.
|
|
if maxlen := 300; len(bs) > maxlen {
|
|
bs = bs[:maxlen]
|
|
}
|
|
return fmt.Errorf("body of %q does not contain %q (got: %q)", url, want, string(bs))
|
|
}
|
|
|
|
return nil
|
|
}
|