fix(telegram): redact bot token from transport error messages

Transport-level failures return *url.Error embedding the full request URL, which contains the bot token. Strip the token before the error propagates to logs.
This commit is contained in:
tiennm99 committed 2026-06-26 14:01:28 +07:00
1 parent 546e6fcde2
commit 0e7967feb6
2 files changed
+43 -1

No files matched your search

+20 -1
View File
@@ -15,6 +15,7 @@ import (
type Client struct {
baseURL string
token string
httpClient *http.Client
requestTimeout time.Duration
}
@@ -66,6 +67,7 @@ func IsTerminalSendError(err error) bool {
func NewClient(token string, timeout time.Duration) *Client {
return &Client{
baseURL: "https://api.telegram.org/bot" + token,
token: token,
httpClient: &http.Client{},
requestTimeout: timeout,
}
@@ -124,6 +126,20 @@ func (c *Client) SendText(ctx context.Context, chatID int64, threadID *int, text
return c.postJSON(ctx, "/sendMessage", payload, &result, c.requestTimeout)
}
// redactToken returns an error whose message has the bot token replaced with a
// placeholder. It leaves the error untouched when no token is set or the token
// does not appear in the message, so typed errors are preserved where possible.
func (c *Client) redactToken(err error) error {
if err == nil || c.token == "" {
return err
}
msg := err.Error()
if !strings.Contains(msg, c.token) {
return err
}
return errors.New(strings.ReplaceAll(msg, c.token, "<redacted>"))
}
func (c *Client) longPollRequestTimeout(timeoutSeconds int) time.Duration {
telegramWait := time.Duration(timeoutSeconds) * time.Second
if telegramWait < 0 {
@@ -160,7 +176,10 @@ func (c *Client) postJSON(ctx context.Context, path string, payload any, target
}
res, err := httpClient.Do(req)
if err != nil {
return err
// Transport errors are *url.Error and embed the full request URL,
// which contains the bot token. Strip it before the error propagates
// to logs.
return c.redactToken(err)
}
defer res.Body.Close()
+23
View File
@@ -6,6 +6,7 @@ import (
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
@@ -168,6 +169,28 @@ func TestSendTextRejectsNonSuccessHTTPStatusEvenWhenEnvelopeOK(t *testing.T) {
}
}
func TestTransportErrorRedactsBotToken(t *testing.T) {
const token = "123456:super-secret-token"
// Port 0 is reserved and never listens, forcing a transport-level dial
// error whose *url.Error message embeds the token-bearing URL.
client := &Client{
baseURL: "http://127.0.0.1:0/bot" + token,
token: token,
httpClient: &http.Client{},
requestTimeout: time.Second,
}
err := client.SendText(context.Background(), 123, nil, "hello")
if err == nil {
t.Fatal("expected transport error, got nil")
}
if strings.Contains(err.Error(), token) {
t.Fatalf("error leaked bot token: %v", err)
}
if !strings.Contains(err.Error(), "<redacted>") {
t.Fatalf("error missing redaction placeholder: %v", err)
}
}
func TestNewClientUsesConfiguredTimeoutForOrdinaryRequests(t *testing.T) {
client := NewClient("token", 10*time.Second)
if client.requestTimeout != 10*time.Second {