Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ func (c *Client) sleep(d time.Duration) {

const maxRetries = 3

var linkNextRe = regexp.MustCompile(`<([^>]+)>;\s*rel="next"`)

// doWithRetry wraps HTTPClient.Do with retry logic for 429 and 5xx responses.
// Only retries GET/DELETE/PUT on 5xx; POST/PATCH only retry on 429.
func (c *Client) doWithRetry(req *http.Request) (*http.Response, error) {
Expand Down Expand Up @@ -305,16 +307,24 @@ func resetBody(req *http.Request) {
// parseRetryAfter parses the Retry-After header value as seconds.
// Returns a default of 1 second if the header is missing or unparseable.
func parseRetryAfter(value string) time.Duration {
const maxRetryDelay = 300 // 5 minutes; anything larger is almost certainly bogus

if value == "" {
return time.Second
}
if seconds, err := strconv.Atoi(value); err == nil && seconds > 0 {
if seconds > maxRetryDelay {
seconds = maxRetryDelay
}
return time.Duration(seconds) * time.Second
}
// Try HTTP-date format
if t, err := http.ParseTime(value); err == nil {
delay := time.Until(t)
if delay > 0 {
if delay > maxRetryDelay*time.Second {
return maxRetryDelay * time.Second
}
return delay
}
}
Expand Down Expand Up @@ -357,9 +367,7 @@ func parseLinkNext(linkHeader string) string {
if linkHeader == "" {
return ""
}
// Parse Link header: <url>; rel="next"
re := regexp.MustCompile(`<([^>]+)>;\s*rel="next"`)
matches := re.FindStringSubmatch(linkHeader)
matches := linkNextRe.FindStringSubmatch(linkHeader)
if len(matches) > 1 {
return matches[1]
}
Expand Down
4 changes: 4 additions & 0 deletions internal/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,10 @@ func TestParseRetryAfter(t *testing.T) {
}{
{"empty", "", time.Second},
{"seconds", "5", 5 * time.Second},
{"capped", "600", 300 * time.Second},
{"overflow", "9227000000", 300 * time.Second},
{"zero", "0", time.Second},
{"negative", "-1", time.Second},
{"invalid", "not-a-number", time.Second},
}
for _, tt := range tests {
Expand Down
45 changes: 45 additions & 0 deletions internal/client/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package client

import "testing"

func FuzzParseLinkNext(f *testing.F) {
f.Add("")
f.Add(`<https://api.example.com/page2>; rel="next"`)
f.Add(`<https://api.example.com/page1>; rel="prev", <https://api.example.com/page3>; rel="next"`)
f.Add(`<https://api.example.com/page1>; rel="prev"`)
f.Add(`<>; rel="next"`)
f.Add(`malformed header`)

f.Fuzz(func(t *testing.T, header string) {
parseLinkNext(header) // must not panic
})
Comment thread
jeremy marked this conversation as resolved.
}

func FuzzParseRetryAfter(f *testing.F) {
f.Add("")
f.Add("5")
f.Add("0")
f.Add("-1")
f.Add("999999999")
f.Add("not-a-number")
f.Add("Wed, 21 Oct 2015 07:28:00 GMT")

f.Fuzz(func(t *testing.T, value string) {
d := parseRetryAfter(value)
if d < 0 {
t.Errorf("parseRetryAfter(%q) returned negative duration: %v", value, d)
}
})
}

func FuzzParsePage(f *testing.F) {
f.Add("")
f.Add("https://api.example.com/cards.json?page=2")
f.Add("https://api.example.com/cards.json")
f.Add("not-a-url")
f.Add("?page=abc")

f.Fuzz(func(t *testing.T, nextURL string) {
ParsePage(nextURL) // must not panic
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
string("9227000000")
49 changes: 49 additions & 0 deletions internal/commands/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package commands

import "testing"

func FuzzParsePseudoColumnID(f *testing.F) {
f.Add("")
f.Add("not-now")
f.Add("NOT_NOW")
f.Add("maybe")
f.Add("maybe?")
f.Add("triage")
f.Add("done")
f.Add("closed")
f.Add("close")
f.Add("random-string")
f.Add(" done ")

f.Fuzz(func(t *testing.T, id string) {
parsePseudoColumnID(id) // must not panic
})
}

func FuzzNormalizeSkillPath(f *testing.F) {
f.Add("")
f.Add("~/skills")
f.Add("~/skills/fizzy")
f.Add("~/skills/fizzy/SKILL.md")
f.Add("/tmp/custom/path")
f.Add("/tmp/custom/path/fizzy")
f.Add("relative/path")
f.Add("file.md")

f.Fuzz(func(t *testing.T, path string) {
normalizeSkillPath(path) // must not panic
})
}

func FuzzExpandPath(f *testing.F) {
f.Add("")
f.Add("~")
f.Add("~/")
f.Add("~/foo/bar")
f.Add("/absolute/path")
f.Add("relative/path")

f.Fuzz(func(t *testing.T, path string) {
expandPath(path) // must not panic
})
}
Loading