Skip to content

feat: Obey Octopus Server rate limits - #493

Merged
YuKitsune merged 9 commits into
mainfrom
eoin/sf/rate-limiting
Oct 16, 2026
Merged

YuKitsune merged 9 commits into
mainfrom
eoin/sf/rate-limiting

Conversation

@YuKitsune

@YuKitsune YuKitsune commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Adds the option to respect the rate limiting headers returned by Octopus Server.

The GCRA algorithm is copied from the TypeScript client used in the frontend. The defaults are also copied from the TS client.

GCRA: https://brandur.org/rate-limiting

@YuKitsune YuKitsune changed the title Respect rate limiting headers feat: Respect rate limiting headers Oct 15, 2026

@YuKitsune YuKitsune left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The design is good and follows normal Go practice: a RoundTripper wrapper, a client helper that does not change base, retries only when GetBody is set, and waits that stop when the context is cancelled. There are no blocking issues. See the inline comments for two fixes and some smaller items.

Also: http.Client.Timeout includes the retry and throttle waits. If Timeout < MaxWait, a retry can fail with a timeout. A note in the README or the NewClient doc would help.

Reviewed by Claude Code.

Comment thread pkg/client/ratelimit/transport.go Resolved
Comment thread pkg/client/ratelimit/transport.go Resolved
Comment thread pkg/client/ratelimit/transport_test.go Resolved
@YuKitsune YuKitsune changed the title feat: Respect rate limiting headers feat: Obey Octopus Server rate limits Oct 15, 2026
@YuKitsune
YuKitsune marked this pull request as ready for review October 15, 2026 05:29
YuKitsune and others added 7 commits October 15, 2026 15:33
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ient

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Default MaxWait when it is 0, pass CloseIdleConnections to the base
transport, and remove upper-bound timing checks from the tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@YuKitsune
YuKitsune force-pushed the eoin/sf/rate-limiting branch from db6bf13 to 40b04b4 Compare October 15, 2026 05:33
Port the GCRA pacer and the retry delay from the C# and TypeScript clients,
with their test cases. Rename the options to match, and default to 5 retries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@sathvikkumar-octo sathvikkumar-octo left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved it, some nits that I'll let you consider

Comment thread pkg/client/ratelimit/transport.go Outdated
const (
// PolicyHeader is the advisory header that tells the burst limit and the requests per minute.
// For example: "l=200;rpm=600".
PolicyHeader = "Octopus-RateLimit-Policy"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to export PolicyHeader or RateLimitHeader from this package?

Comment thread pkg/client/ratelimit/transport_test.go Outdated
require.Equal(t, 3, server.requestCount())
}

func TestRoundTripWaitsForRetryAfter(t *testing.T) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, non-blocking: A few tests overlap and could go to keep the suite lean:

  • TestRoundTripWaitsForRetryAfter sleeps a real 1s. retry_test covers the Retry-After delay, and …WaitsTheDefaultDelay… already shows RoundTrip waits.
  • TestRetryDelayRetriesImmediatelyAtEachAttemptWhenRetryAfterIsZero is the same as the Zero case.
  • TestNewHttpClientKeepsTheSettingsOfBase only tests Go's struct copy.
  • TestNewHttpClientRetriesTooManyRequests and …CloseIdleConnectionsReachesTheBaseTransport are covered by the transport tests.
  • TestNewClientWithoutRateLimitedHttpClientReturnsTooManyRequests tests the client without rate limiting, which this PR doesn't change.
  • TestDefaultOptions restates the defaults.

TestNewTransportPacesOnlyWhenUseRateLimitHeadersIsTrue checks the private pacer field. A behaviour test would cover the same thing without depending on internals: with UseRateLimitHeaders: false, an r=0 response shouldn't cause a wait.

@YuKitsune
YuKitsune enabled auto-merge October 16, 2026 00:51
@YuKitsune
YuKitsune merged commit 2a3b506 into main Oct 16, 2026
6 checks passed
@YuKitsune
YuKitsune deleted the eoin/sf/rate-limiting branch October 16, 2026 01:00
YuKitsune added a commit to OctopusDeploy/cli that referenced this pull request Oct 16, 2026
Depends on OctopusDeploy/go-octopusdeploy#493

Upgrades `go-octopusdeploy` to v2.122.0, and starts using the new
rate-limiting HTTP client with the default settings.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants