feat(github): add safeguards against accidental AllowInsecureHTTP use (#96) #113
@@ -8,7 +8,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -97,6 +99,10 @@ type Client struct {
|
||||
token string
|
||||
httpClient *http.Client
|
||||
|
|
||||
|
||||
// allowInsecureHTTP permits requests to HTTP (non-TLS) endpoints.
|
||||
// When false, doRequest rejects URLs with an http:// scheme.
|
||||
allowInsecureHTTP bool
|
||||
|
sonnet-review-bot
commented
[NIT] The doc comment on **[NIT]** The doc comment on `WithAllowInsecureHTTP` says 'a warning is logged' when the env var is absent, but this is more precisely described as 'the option is silently ignored and a warning is emitted'. The current wording is slightly ambiguous about whether the option takes partial effect. Very minor — the comment in NewClient is accurate.
|
||||
|
||||
// retryBackoff defines the delays between retry attempts for 429 responses.
|
||||
// retryBackoff[i] is the delay before attempt i+1 (after attempt i fails).
|
||||
// If nil, defaults to {1s, 2s}.
|
||||
@@ -135,16 +141,63 @@ func defaultCheckRedirect(req *http.Request, via []*http.Request) error {
|
||||
return nil
|
||||
|
gpt-review-bot
commented
[MINOR] Introduces a functional options API (ClientOption) for a single option. Per configuration patterns, functional options are best when option sets are larger/growing; for a single toggle, a config struct or explicit parameter may be simpler. Not a blocker but consider whether this extra abstraction is warranted. **[MINOR]** Introduces a functional options API (ClientOption) for a single option. Per configuration patterns, functional options are best when option sets are larger/growing; for a single toggle, a config struct or explicit parameter may be simpler. Not a blocker but consider whether this extra abstraction is warranted.
|
||||
}
|
||||
|
||||
// ClientOption configures optional behavior of a Client.
|
||||
type ClientOption func(*clientConfig)
|
||||
|
||||
type clientConfig struct {
|
||||
allowInsecureHTTP bool
|
||||
insecureIsTestBypass bool
|
||||
}
|
||||
|
||||
// AllowInsecureHTTP permits sending credentials over plaintext HTTP connections.
|
||||
// In production, this option is gated by the REVIEW_BOT_ALLOW_INSECURE=1
|
||||
// environment variable. Without the env var set, the option is silently ignored
|
||||
// and a warning is logged.
|
||||
//
|
||||
// For tests, prefer AllowInsecureHTTPForTest which bypasses the env gate.
|
||||
|
sonnet-review-bot
commented
[MINOR] AllowInsecureHTTPForTest is in the production file (client.go). Per the convention, test-only helpers should ideally live in an export_test.go file or be clearly gated. Since this function is exported and intended exclusively for test code, it bleeds test surface into the production API. Consider moving it to a file compiled only during tests (e.g., export_test.go), or renaming to make its test-only nature even more prominent in godoc. **[MINOR]** AllowInsecureHTTPForTest is in the production file (client.go). Per the convention, test-only helpers should ideally live in an export_test.go file or be clearly gated. Since this function is exported and intended exclusively for test code, it bleeds test surface into the production API. Consider moving it to a file compiled only during tests (e.g., export_test.go), or renaming to make its test-only nature even more prominent in godoc.
|
||||
func AllowInsecureHTTP() ClientOption {
|
||||
return func(cfg *clientConfig) {
|
||||
cfg.allowInsecureHTTP = true
|
||||
|
gpt-review-bot
commented
[MINOR] AllowInsecureHTTPForTest is exported but intended only for tests. Consider making it unexported (allowInsecureHTTPForTest) and using it from package-internal tests, or clearly document and enforce via build tags/export_test.go if cross-package tests require it, to reduce risk of accidental production use. **[MINOR]** AllowInsecureHTTPForTest is exported but intended only for tests. Consider making it unexported (allowInsecureHTTPForTest) and using it from package-internal tests, or clearly document and enforce via build tags/export_test.go if cross-package tests require it, to reduce risk of accidental production use.
gpt-review-bot
commented
[NIT] For functional options, consider a With* naming convention (e.g., WithInsecureHTTP) to align with common Go patterns and improve discoverability (see configuration.md, Functional Options). **[NIT]** For functional options, consider a With* naming convention (e.g., WithInsecureHTTP) to align with common Go patterns and improve discoverability (see configuration.md, Functional Options).
|
||||
}
|
||||
}
|
||||
|
sonnet-review-bot
commented
[NIT] The doc comment for AllowInsecureHTTP() has an overly long line: 'For tests, use AllowInsecureHTTPForTest (defined in a _test.go file in the same package) which bypasses the env gate.' This wraps beyond 80 chars and doesn't follow the conventional line-length style seen elsewhere in the file, though this is cosmetic only. **[NIT]** The doc comment for AllowInsecureHTTP() has an overly long line: 'For tests, use AllowInsecureHTTPForTest (defined in a _test.go file in the same package) which bypasses the env gate.' This wraps beyond 80 chars and doesn't follow the conventional line-length style seen elsewhere in the file, though this is cosmetic only.
|
||||
|
||||
|
sonnet-review-bot
commented
[MINOR] The doc comment for **[MINOR]** The doc comment for `AllowInsecureHTTP()` says the option is 'silently ignored' but the very next sentence says 'a warning is logged'. These are contradictory — being warned in logs is not silent. The comment should say something like 'the option is ignored and a warning is logged via slog.Warn'.
|
||||
// AllowInsecureHTTPForTest permits sending credentials over plaintext HTTP
|
||||
// without requiring the REVIEW_BOT_ALLOW_INSECURE environment variable.
|
||||
|
gpt-review-bot
commented
[NIT] Doc comment for NewClient references WithAllowInsecureHTTPForTest, which is a test-only symbol and not part of the production API. This could confuse users reading package docs; consider rephrasing to avoid mentioning test-only helpers in production documentation. **[NIT]** Doc comment for NewClient references WithAllowInsecureHTTPForTest, which is a test-only symbol and not part of the production API. This could confuse users reading package docs; consider rephrasing to avoid mentioning test-only helpers in production documentation.
|
||||
// This is intended exclusively for test code using httptest.Server.
|
||||
func AllowInsecureHTTPForTest() ClientOption {
|
||||
|
gpt-review-bot
commented
[MINOR] NewClient signature changed to accept variadic options (opts ...ClientOption), which is a breaking API change for callers using the previous signature. Consider providing a backward-compatible wrapper (overload pattern) or calling out the breaking change explicitly in release notes. **[MINOR]** NewClient signature changed to accept variadic options (opts ...ClientOption), which is a breaking API change for callers using the previous signature. Consider providing a backward-compatible wrapper (overload pattern) or calling out the breaking change explicitly in release notes.
|
||||
return func(cfg *clientConfig) {
|
||||
|
[MINOR] AllowInsecureHTTPForTest bypasses the env gate and does not emit a warning; as an exported option it could be mistakenly used in production, silently allowing plaintext HTTP. Consider restricting its availability to tests (e.g., define in a _test.go file) or emitting a warning when enabled to preserve operator visibility. **[MINOR]** AllowInsecureHTTPForTest bypasses the env gate and does not emit a warning; as an exported option it could be mistakenly used in production, silently allowing plaintext HTTP. Consider restricting its availability to tests (e.g., define in a _test.go file) or emitting a warning when enabled to preserve operator visibility.
|
||||
cfg.allowInsecureHTTP = true
|
||||
cfg.insecureIsTestBypass = true
|
||||
}
|
||||
|
sonnet-review-bot
commented
[MINOR] **[MINOR]** `AllowInsecureHTTPForTest()` is an exported function in the production package `github`. Per the `export_test.go` pattern (testing-advanced.md #11), test-only helpers that expose internals belong in an `export_test.go` file (package `github`, compiled only during tests), not in the main production code. As written, `AllowInsecureHTTPForTest` is callable by any consumer of the package, not just tests. This is a minor concern since the function is clearly named and documented, but it leaks a test helper into the public API surface.
sonnet-review-bot
commented
[NIT] The doc comment for **[NIT]** The doc comment for `AllowInsecureHTTP()` says 'silently ignored' but the code actually logs a `slog.Warn` — it's not silent. The comment should say 'ignored with a warning log' to match actual behavior.
|
||||
}
|
||||
|
||||
// NewClient creates a new GitHub API client.
|
||||
// If baseURL is empty, it defaults to https://api.github.com.
|
||||
|
sonnet-review-bot
commented
[MINOR] The doc comment for AllowInsecureHTTP() says 'For tests, use AllowInsecureHTTPForTest (defined in export_test.go)' but export_test.go is actually in the package itself (package github), not a separate file the doc comment navigation implies. This is fine technically but the phrasing slightly misrepresents the location — it's defined in a _test.go file in the same package. Minor wording issue only. **[MINOR]** The doc comment for AllowInsecureHTTP() says 'For tests, use AllowInsecureHTTPForTest (defined in export_test.go)' but export_test.go is actually in the package itself (package github), not a separate file the doc comment navigation implies. This is fine technically but the phrasing slightly misrepresents the location — it's defined in a _test.go file in the same package. Minor wording issue only.
|
||||
// For GitHub Enterprise, pass the API base URL (e.g. https://github.concur.com/api/v3).
|
||||
|
gpt-review-bot
commented
[NIT] The environment variable key "REVIEW_BOT_ALLOW_INSECURE" is repeated; consider extracting it into a package-level const to avoid typos and ease discovery. **[NIT]** The environment variable key "REVIEW_BOT_ALLOW_INSECURE" is repeated; consider extracting it into a package-level const to avoid typos and ease discovery.
gpt-review-bot
commented
[NIT] Constructing a client with WithAllowInsecureHTTP logs warnings via slog.Warn. While intentional, logging side effects in constructors can surprise library consumers. Consider documenting this behavior prominently in package docs or providing a way to suppress logs if needed. **[NIT]** Constructing a client with WithAllowInsecureHTTP logs warnings via slog.Warn. While intentional, logging side effects in constructors can surprise library consumers. Consider documenting this behavior prominently in package docs or providing a way to suppress logs if needed.
|
||||
func NewClient(token, baseURL string) *Client {
|
||||
func NewClient(token, baseURL string, opts ...ClientOption) *Client {
|
||||
if baseURL == "" {
|
||||
baseURL = defaultBaseURL
|
||||
|
sonnet-review-bot
commented
[MINOR] The env-gate silently ignores AllowInsecureHTTP (logs a warning but doesn't return an error) when REVIEW_BOT_ALLOW_INSECURE is absent. This is a deliberate design choice documented in the PR, but it's a subtle failure mode: a developer who calls AllowInsecureHTTP() expecting HTTP to work will instead get a 'refusing HTTP request' error from doRequest with no obvious connection to the silent warning at construction time. Returning an error from NewClient or panicking at construction time would make the misconfiguration immediately obvious. This is a design trade-off rather than a bug, but worth noting. **[MINOR]** The env-gate silently ignores AllowInsecureHTTP (logs a warning but doesn't return an error) when REVIEW_BOT_ALLOW_INSECURE is absent. This is a deliberate design choice documented in the PR, but it's a subtle failure mode: a developer who calls AllowInsecureHTTP() expecting HTTP to work will instead get a 'refusing HTTP request' error from doRequest with no obvious connection to the silent warning at construction time. Returning an error from NewClient or panicking at construction time would make the misconfiguration immediately obvious. This is a design trade-off rather than a bug, but worth noting.
|
||||
}
|
||||
|
||||
|
sonnet-review-bot
commented
[MINOR] The slog.Warn call for the 'option ignored' case concatenates strings at call time ( **[MINOR]** The slog.Warn call for the 'option ignored' case concatenates strings at call time (`"WithAllowInsecureHTTP option ignored: set "+envAllowInsecure+"=1 to enable"`). This is a trivial allocation on an error path so it has no practical impact, but idiomatic slog usage would pass the env var name as a structured attribute rather than embedding it in the message string, e.g. `slog.Warn("WithAllowInsecureHTTP option ignored", "hint", "set "+envAllowInsecure+"=1 to enable")` or a literal string. This is purely stylistic.
|
||||
var cfg clientConfig
|
||||
for _, opt := range opts {
|
||||
opt(&cfg)
|
||||
}
|
||||
|
||||
|
sonnet-review-bot
commented
[MINOR] NewClient reads os.Getenv at construction time, which makes the env-gate check susceptible to a subtle race: if a caller sets the env var after program startup but before calling NewClient (rare but possible in CLI tools), the behavior is correct, but the deeper issue is that env reads in constructors make testing harder and can be surprising. The pattern is pragmatic here and t.Setenv handles it in tests, but worth noting that the test-bypass field (testBypass) in clientConfig is part of the exported clientConfig type — if clientConfig is ever exported or accessed via reflection, testBypass would be visible. Since it's unexported, this is fine as-is. **[MINOR]** NewClient reads os.Getenv at construction time, which makes the env-gate check susceptible to a subtle race: if a caller sets the env var after program startup but before calling NewClient (rare but possible in CLI tools), the behavior is correct, but the deeper issue is that env reads in constructors make testing harder and can be surprising. The pattern is pragmatic here and t.Setenv handles it in tests, but worth noting that the test-bypass field (testBypass) in clientConfig is part of the exported clientConfig type — if clientConfig is ever exported or accessed via reflection, testBypass would be visible. Since it's unexported, this is fine as-is.
|
||||
if cfg.allowInsecureHTTP && !cfg.insecureIsTestBypass {
|
||||
if os.Getenv("REVIEW_BOT_ALLOW_INSECURE") != "1" {
|
||||
slog.Warn("AllowInsecureHTTP ignored: set REVIEW_BOT_ALLOW_INSECURE=1 to enable")
|
||||
cfg.allowInsecureHTTP = false
|
||||
} else {
|
||||
slog.Warn("AllowInsecureHTTP enabled — credentials may be sent over plaintext",
|
||||
|
sonnet-review-bot
commented
[NIT] NewClient now reads os.Getenv at construction time and calls slog.Warn, creating a side effect (log output) when constructing a client with AllowInsecureHTTP() but without the env var. The conventions doc says 'never panic; return errors' and the pattern docs suggest constructors should avoid surprising side effects. The silent-ignore + warn behavior is a deliberate design choice documented in the PR, but callers who pass AllowInsecureHTTP() will be silently downgraded to a restricted client with no feedback other than a log line — a future caller who forgets the env var in production will get unexpected 'refusing to send credentials' errors from doRequest that don't clearly indicate the option was silently ignored at construction. Returning an error from NewClient or using a different API shape (e.g., requiring the env-gate check to be explicit) would be cleaner, but this is a design tradeoff, not a bug. **[NIT]** NewClient now reads os.Getenv at construction time and calls slog.Warn, creating a side effect (log output) when constructing a client with AllowInsecureHTTP() but without the env var. The conventions doc says 'never panic; return errors' and the pattern docs suggest constructors should avoid surprising side effects. The silent-ignore + warn behavior is a deliberate design choice documented in the PR, but callers who pass AllowInsecureHTTP() will be silently downgraded to a restricted client with no feedback other than a log line — a future caller who forgets the env var in production will get unexpected 'refusing to send credentials' errors from doRequest that don't clearly indicate the option was silently ignored at construction. Returning an error from NewClient or using a different API shape (e.g., requiring the env-gate check to be explicit) would be cleaner, but this is a design tradeoff, not a bug.
|
||||
"env", "REVIEW_BOT_ALLOW_INSECURE=1")
|
||||
|
sonnet-review-bot
commented
[NIT] The warn log 'AllowInsecureHTTP ignored: set REVIEW_BOT_ALLOW_INSECURE=1 to enable' fires silently in tests that run in environments where the env var isn't set. Since NewClient is called in many tests (e.g. TestNewClient_DefaultBaseURL, TestParseRetryAfter_* etc.) without AllowInsecureHTTP(), this won't fire there. But any caller using AllowInsecureHTTP() in a test without the env var will produce a slog.Warn to whatever slog default is configured. The AllowInsecureHTTPForTest bypass prevents this for test servers, so this is acceptable — just worth noting that the warn will fire in non-test environments where someone misconfigures the option. **[NIT]** The warn log 'AllowInsecureHTTP ignored: set REVIEW_BOT_ALLOW_INSECURE=1 to enable' fires silently in tests that run in environments where the env var isn't set. Since NewClient is called in many tests (e.g. TestNewClient_DefaultBaseURL, TestParseRetryAfter_* etc.) without AllowInsecureHTTP(), this won't fire there. But any caller using AllowInsecureHTTP() in a test without the env var will produce a slog.Warn to whatever slog default is configured. The AllowInsecureHTTPForTest bypass prevents this for test servers, so this is acceptable — just worth noting that the warn will fire in non-test environments where someone misconfigures the option.
|
||||
}
|
||||
}
|
||||
|
sonnet-review-bot
commented
[MINOR] **[MINOR]** `clientConfig` is an unexported helper struct that only exists to accumulate options before constructing the `Client`. The `insecureIsTestBypass` field is a boolean that flows only through `NewClient`'s env-gate logic and is never stored on `Client` itself. This is fine, but the `clientConfig` type leaks the test/prod distinction up into the option application logic. An alternative would be two separate unexported option types (or a single exported sentinel constant), but the current approach is acceptable and idiomatic enough for a private config accumulator.
|
||||
|
||||
return &Client{
|
||||
|
sonnet-review-bot
commented
[NIT] The alignment of struct literal fields in the return statement is inconsistent: **[NIT]** The alignment of struct literal fields in the return statement is inconsistent: `allowInsecureHTTP:` has extra spaces to align the colon with `baseURL:` and `token:`, but `httpClient:` is not similarly aligned. gofmt should handle this, but if the file was run through gofmt and this is what it produces, it's fine — just worth verifying the file is gofmt-clean.
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
token: token,
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
token: token,
|
||||
allowInsecureHTTP: cfg.allowInsecureHTTP,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
CheckRedirect: defaultCheckRedirect,
|
||||
@@ -215,10 +268,23 @@ func (c *Client) parseRetryAfter(value string) (time.Duration, bool) {
|
||||
return 0, false
|
||||
|
[MINOR] redactURL only strips query parameters and can still leak credentials if present in the userinfo component (e.g., http://user:pass@host/path). Consider parsing with url.Parse and redacting u.User and fragment to avoid potential secret exposure in logs. **[MINOR]** redactURL only strips query parameters and can still leak credentials if present in the userinfo component (e.g., http://user:pass@host/path). Consider parsing with url.Parse and redacting u.User and fragment to avoid potential secret exposure in logs.
|
||||
}
|
||||
|
||||
|
sonnet-review-bot
commented
[NIT] **[NIT]** `redactURL` uses `strings.IndexByte` which is perfectly fine, but since the project already imports `net/url`, using `url.Parse` + clearing `RawQuery` would handle edge cases like fragment identifiers (`#`) and malformed URLs more robustly. Not a bug given the controlled usage context (internal URLs), just a note.
gpt-review-bot
commented
[NIT] redactURL redacts userinfo and query parameters; consider also clearing URL fragments (u.Fragment) to avoid leaking anchor components in logs, even though they’re generally not sent to servers. **[NIT]** redactURL redacts userinfo and query parameters; consider also clearing URL fragments (u.Fragment) to avoid leaking anchor components in logs, even though they’re generally not sent to servers.
|
||||
// redactURL redacts query parameters from a URL for safe inclusion in error
|
||||
// messages and log output.
|
||||
func redactURL(rawURL string) string {
|
||||
|
sonnet-review-bot
commented
[MINOR] hasHTTPSScheme uses strings.EqualFold for the scheme prefix check. Since "https://" contains only ASCII characters, EqualFold is correct but adds a small amount of unnecessary overhead compared to a direct strings.ToLower or a case-sensitive check (HTTP schemes per RFC 3986 are case-insensitive but in practice always lowercase in this codebase). This is purely a performance NIT on a non-hot path and does not affect correctness. **[MINOR]** hasHTTPSScheme uses strings.EqualFold for the scheme prefix check. Since "https://" contains only ASCII characters, EqualFold is correct but adds a small amount of unnecessary overhead compared to a direct strings.ToLower or a case-sensitive check (HTTP schemes per RFC 3986 are case-insensitive but in practice always lowercase in this codebase). This is purely a performance NIT on a non-hot path and does not affect correctness.
|
||||
if idx := strings.IndexByte(rawURL, '?'); idx != -1 {
|
||||
|
security-review-bot marked this conversation as resolved
Outdated
[MINOR] The refusal error includes the full request URL (including query string) which may carry sensitive data in some edge cases; if upstream logs this error verbatim, it could leak information. Consider redacting query parameters or logging a sanitized URL. **[MINOR]** The refusal error includes the full request URL (including query string) which may carry sensitive data in some edge cases; if upstream logs this error verbatim, it could leak information. Consider redacting query parameters or logging a sanitized URL.
|
||||
return rawURL[:idx] + "?<redacted>"
|
||||
}
|
||||
|
[MAJOR] The HTTP-scheme guard in doRequest uses a case-sensitive prefix check (strings.HasPrefix(reqURL, "http://"). URI schemes are case-insensitive (RFC 3986), so a URL like "HTTP://..." bypasses the guard and may send credentials over plaintext. Parse the URL and compare scheme case-insensitively (e.g., url.Parse + strings.EqualFold(u.Scheme, "http")). **[MAJOR]** The HTTP-scheme guard in doRequest uses a case-sensitive prefix check (strings.HasPrefix(reqURL, "http://"). URI schemes are case-insensitive (RFC 3986), so a URL like "HTTP://..." bypasses the guard and may send credentials over plaintext. Parse the URL and compare scheme case-insensitively (e.g., url.Parse + strings.EqualFold(u.Scheme, "http")).
|
||||
return rawURL
|
||||
}
|
||||
|
||||
|
sonnet-review-bot
commented
[NIT] The **[NIT]** The `redactURL` function has a dead code path: `if u.User != nil { u.User = nil }`. The nil check is unnecessary because assigning nil to a nil pointer is a no-op. This works correctly but is slightly misleading. Could simplify to `u.User = nil`.
sonnet-review-bot
commented
[NIT] The HTTP rejection error message says 'use HTTPS or set AllowInsecureHTTP option'. For users who call AllowInsecureHTTP() and see it silently fall back (env var not set), this message may be confusing — they did set the option but it was ignored. A slightly more helpful message might mention the REVIEW_BOT_ALLOW_INSECURE env var. Minor UX nit. **[NIT]** The HTTP rejection error message says 'use HTTPS or set AllowInsecureHTTP option'. For users who call AllowInsecureHTTP() and see it silently fall back (env var not set), this message may be confusing — they did set the option but it was ignored. A slightly more helpful message might mention the REVIEW_BOT_ALLOW_INSECURE env var. Minor UX nit.
|
||||
// doRequest performs an HTTP request with retry on 429 rate limit responses.
|
||||
// It respects the Retry-After header when present, supporting both integer
|
||||
// seconds and HTTP-date formats (capped at maxRetryAfter).
|
||||
|
gpt-review-bot
commented
[MAJOR] The insecure-HTTP guard uses strings.HasPrefix(reqURL, "http://") which is case-sensitive. URL schemes are case-insensitive; a URL like "HTTP://..." would bypass this check and send credentials over plaintext. Parse the URL and compare the scheme case-insensitively (e.g., url.Parse + strings.EqualFold(u.Scheme, "http")) or normalize to lower-case before checking. **[MAJOR]** The insecure-HTTP guard uses strings.HasPrefix(reqURL, "http://") which is case-sensitive. URL schemes are case-insensitive; a URL like "HTTP://..." would bypass this check and send credentials over plaintext. Parse the URL and compare the scheme case-insensitively (e.g., url.Parse + strings.EqualFold(u.Scheme, "http")) or normalize to lower-case before checking.
|
||||
func (c *Client) doRequest(ctx context.Context, method, reqURL string, accept string) ([]byte, error) {
|
||||
if !c.allowInsecureHTTP && strings.HasPrefix(reqURL, "http://") {
|
||||
return nil, fmt.Errorf("refusing HTTP request to %s: use HTTPS or set AllowInsecureHTTP option", redactURL(reqURL))
|
||||
}
|
||||
|
||||
var backoff []time.Duration
|
||||
|
[MINOR] The error message includes user-influenced URL data via redactURL(reqURL) without explicit newline/carriage-return sanitization. If upstream code logs this error directly, it could allow limited log injection if a crafted URL with control characters is accepted by url.Parse. Consider sanitizing \n/\r in the formatted error or ensuring redactURL strips such characters. **[MINOR]** The error message includes user-influenced URL data via redactURL(reqURL) without explicit newline/carriage-return sanitization. If upstream code logs this error directly, it could allow limited log injection if a crafted URL with control characters is accepted by url.Parse. Consider sanitizing \n/\r in the formatted error or ensuring redactURL strips such characters.
|
||||
if c.retryBackoff != nil {
|
||||
backoff = append([]time.Duration(nil), c.retryBackoff...)
|
||||
|
sonnet-review-bot
commented
[NIT] The HTTP scheme check in **[NIT]** The HTTP scheme check in `doRequest` parses the URL twice when the URL is also valid for the request — once here in the guard and implicitly again by `http.NewRequestWithContext`. This is a minor inefficiency in the hot path but not a correctness issue.
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestDoRequest_Success(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient("test-token", srv.URL)
|
||||
c := NewClient("test-token", srv.URL, AllowInsecureHTTPForTest())
|
||||
body, err := c.doGet(context.Background(), srv.URL+"/test")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
@@ -60,7 +60,7 @@ func TestDoRequest_429_RetryAfter_IntegerSeconds(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient("tok", srv.URL)
|
||||
c := NewClient("tok", srv.URL, AllowInsecureHTTPForTest())
|
||||
c.SetRetryBackoff([]time.Duration{0, 0})
|
||||
|
||||
body, err := c.doGet(context.Background(), srv.URL+"/test")
|
||||
@@ -94,7 +94,7 @@ func TestDoRequest_429_RetryAfter_HTTPDate(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient("tok", srv.URL)
|
||||
c := NewClient("tok", srv.URL, AllowInsecureHTTPForTest())
|
||||
c.now = func() time.Time { return fixedNow }
|
||||
// Initial backoff is 0; the HTTP-date parser will compute 1s and override.
|
||||
c.SetRetryBackoff([]time.Duration{0, 0})
|
||||
@@ -130,7 +130,7 @@ func TestDoRequest_429_RetryAfter_HTTPDate_InPast(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient("tok", srv.URL)
|
||||
c := NewClient("tok", srv.URL, AllowInsecureHTTPForTest())
|
||||
c.now = func() time.Time { return fixedNow }
|
||||
c.SetRetryBackoff([]time.Duration{0, 0})
|
||||
|
||||
@@ -157,7 +157,7 @@ func TestDoRequest_429_NoRetryAfter_UsesDefaultBackoff(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient("tok", srv.URL)
|
||||
c := NewClient("tok", srv.URL, AllowInsecureHTTPForTest())
|
||||
c.SetRetryBackoff([]time.Duration{0, 0})
|
||||
|
||||
body, err := c.doGet(context.Background(), srv.URL+"/test")
|
||||
@@ -187,7 +187,7 @@ func TestDoRequest_429_InvalidRetryAfter_UsesDefaultBackoff(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient("tok", srv.URL)
|
||||
c := NewClient("tok", srv.URL, AllowInsecureHTTPForTest())
|
||||
c.SetRetryBackoff([]time.Duration{0, 0})
|
||||
|
||||
body, err := c.doGet(context.Background(), srv.URL+"/test")
|
||||
@@ -208,7 +208,7 @@ func TestDoRequest_404_NoRetry(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient("tok", srv.URL)
|
||||
c := NewClient("tok", srv.URL, AllowInsecureHTTPForTest())
|
||||
_, err := c.doGet(context.Background(), srv.URL+"/test")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
@@ -230,7 +230,7 @@ func TestDoRequest_401_NoRetry(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient("tok", srv.URL)
|
||||
c := NewClient("tok", srv.URL, AllowInsecureHTTPForTest())
|
||||
_, err := c.doGet(context.Background(), srv.URL+"/test")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
@@ -260,7 +260,7 @@ func TestDoRequest_ContextCanceled(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient("tok", srv.URL)
|
||||
c := NewClient("tok", srv.URL, AllowInsecureHTTPForTest())
|
||||
c.SetRetryBackoff([]time.Duration{10 * time.Second, 10 * time.Second})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -511,3 +511,108 @@ func TestSetHTTPClient_NilRestoresDefault(t *testing.T) {
|
||||
t.Fatal("expected CheckRedirect policy after SetHTTPClient(nil)")
|
||||
}
|
||||
}
|
||||
|
sonnet-review-bot
commented
[NIT] The three new **[NIT]** The three new `TestDoRequest_RejectsHTTPWithToken*` / `TestDoRequest_AllowsHTTP*` tests and the `TestWithAllowInsecureHTTP_*` tests each spin up a dedicated `httptest.Server` even when the server is never contacted (e.g., the 'rejects' tests). This is harmless but slightly wasteful. An alternative would be to test `doGet` directly against a static HTTP URL like `"http://example.com/test"` without a real server for rejection tests. Not a bug.
|
||||
|
||||
|
sonnet-review-bot
commented
[NIT] **[NIT]** `TestAllowInsecureHTTPForTest_PermitsHTTP` is functionally identical to `TestDoRequest_Success` (both create an httptest.NewServer, use AllowInsecureHTTPForTest, and check the body). While the duplication is harmless, consolidating or removing the redundant test would reduce noise.
|
||||
func TestAllowInsecureHTTPForTest_PermitsHTTP(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient("tok", srv.URL, AllowInsecureHTTPForTest())
|
||||
body, err := c.doGet(context.Background(), srv.URL+"/test")
|
||||
|
sonnet-review-bot
commented
[MINOR] There is a double blank line between **[MINOR]** There is a double blank line between `TestDoRequest_RejectsHTTPWithToken` and `TestDoRequest_RejectsHTTPWithToken_RedactsQueryParams`. Similarly, there's a missing blank line before `TestDoRequest_AllowsHTTPWithoutToken` at line 556. These are minor formatting inconsistencies that `gofmt` would not catch (gofmt only enforces indentation, not blank lines between top-level declarations), but the repository style convention says 'Keep commits atomic and well-described' and the style pattern says one blank line between top-level declarations.
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if string(body) != "ok" {
|
||||
t.Errorf("body = %q, want %q", body, "ok")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoInsecureOption_RejectsHTTP(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("request should not have been sent")
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
sonnet-review-bot
commented
[NIT] TestNoInsecureOption_RejectsUppercaseHTTP and TestNoInsecureOption_RejectsMixedCaseHTTP don't start an httptest.Server — they just pass 'HTTP://example.com/test' directly. These tests don't need a real server (good), but using an unreachable external host in test code is a minor smell. Consider using 'HTTP://127.0.0.1:1/test' or similar to make it obvious no real connection is attempted. Not a bug since the scheme check fires before any TCP connection. **[NIT]** TestNoInsecureOption_RejectsUppercaseHTTP and TestNoInsecureOption_RejectsMixedCaseHTTP don't start an httptest.Server — they just pass 'HTTP://example.com/test' directly. These tests don't need a real server (good), but using an unreachable external host in test code is a minor smell. Consider using 'HTTP://127.0.0.1:1/test' or similar to make it obvious no real connection is attempted. Not a bug since the scheme check fires before any TCP connection.
|
||||
|
||||
|
sonnet-review-bot
commented
[NIT] TestDoRequest_AllowsHTTPWithoutToken passes AllowInsecureHTTPForTest() even though the token is empty and the HTTPS check is conditioned on c.token != "". The test is slightly misleading: it would pass without AllowInsecureHTTPForTest() at all. Consider removing the option from this test to better document that no-token HTTP is allowed unconditionally, which is the actual behavior being tested. **[NIT]** TestDoRequest_AllowsHTTPWithoutToken passes AllowInsecureHTTPForTest() even though the token is empty and the HTTPS check is conditioned on c.token != "". The test is slightly misleading: it would pass without AllowInsecureHTTPForTest() at all. Consider removing the option from this test to better document that no-token HTTP is allowed unconditionally, which is the actual behavior being tested.
|
||||
c := NewClient("tok", srv.URL)
|
||||
_, err := c.doGet(context.Background(), srv.URL+"/test")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for HTTP request without AllowInsecureHTTP")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "refusing HTTP request") {
|
||||
t.Errorf("unexpected error message: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowInsecureHTTP_WithoutEnvVar_Rejected(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("request should not have been sent")
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
t.Setenv("REVIEW_BOT_ALLOW_INSECURE", "")
|
||||
|
sonnet-review-bot
commented
[NIT] Missing blank line between **[NIT]** Missing blank line between `TestNoInsecureOption_RejectsHTTP` and `TestNoInsecureOption_RejectsUppercaseHTTP` — minor formatting inconsistency compared to the rest of the file, but `gofmt` doesn't enforce blank lines between top-level declarations in the same way.
|
||||
|
||||
c := NewClient("tok", srv.URL, AllowInsecureHTTP())
|
||||
_, err := c.doGet(context.Background(), srv.URL+"/test")
|
||||
if err == nil {
|
||||
t.Fatal("expected error: AllowInsecureHTTP without env var should be rejected")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "refusing HTTP request") {
|
||||
t.Errorf("unexpected error message: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowInsecureHTTP_WithEnvVar_Permitted(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("insecure-ok"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
t.Setenv("REVIEW_BOT_ALLOW_INSECURE", "1")
|
||||
|
||||
c := NewClient("tok", srv.URL, AllowInsecureHTTP())
|
||||
body, err := c.doGet(context.Background(), srv.URL+"/test")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if string(body) != "insecure-ok" {
|
||||
t.Errorf("body = %q, want %q", body, "insecure-ok")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowInsecureHTTP_EnvVarNotOne_Rejected(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("request should not have been sent")
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
// "true" is not "1" — strict check
|
||||
t.Setenv("REVIEW_BOT_ALLOW_INSECURE", "true")
|
||||
|
||||
c := NewClient("tok", srv.URL, AllowInsecureHTTP())
|
||||
_, err := c.doGet(context.Background(), srv.URL+"/test")
|
||||
if err == nil {
|
||||
t.Fatal("expected error: env var 'true' is not '1'")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "refusing HTTP request") {
|
||||
t.Errorf("unexpected error message: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactURL_WithQuery(t *testing.T) {
|
||||
got := redactURL("http://localhost:1234/path?secret=token&foo=bar")
|
||||
want := "http://localhost:1234/path?<redacted>"
|
||||
if got != want {
|
||||
t.Errorf("redactURL = %q, want %q", got, want)
|
||||
|
sonnet-review-bot
commented
[NIT] Double blank line before **[NIT]** Double blank line before `TestAllowInsecureHTTP_WithoutEnvVar_Rejected` — minor inconsistency with the rest of the file's style.
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactURL_NoQuery(t *testing.T) {
|
||||
got := redactURL("http://localhost:1234/path")
|
||||
want := "http://localhost:1234/path"
|
||||
if got != want {
|
||||
t.Errorf("redactURL = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
[MINOR] The functional option function is named AllowInsecureHTTP rather than following the documented With* naming convention (e.g., WithAllowInsecureHTTP). Consider aligning with the 'Functional Options (With* Pattern)' for consistency.