Skip to content

Commit db7b066

Browse files
christiangdaclaude
andcommitted
fix: replace httpretrier with httpx for proper 429 retry handling
The httpretrier library did not properly retry HTTP 429 (Too Many Requests) responses from the AWS SSO SCIM API. Replace it with httpx which explicitly retries on 429 and 5xx errors with configurable backoff strategies. AWS SCIM clients now use jitter backoff to prevent thundering herd effects during rate limiting. Google Workspace clients use exponential backoff. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8125aba commit db7b066

7 files changed

Lines changed: 69 additions & 55 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ sam deploy --parameter-overrides SyncUserFields=phoneNumbers,addresses,enterpris
191191
## ⚠️ Limitations
192192

193193
* **Group Limit**: The AWS SSO SCIM API has a limit of 50 groups per request. Please support the feature request on the [AWS Support site](https://repost.aws/questions/QUqqnVkIo_SYyF_SlX5LcUjg/aws-sso-scim-api-pagination-for-methods) to help get this limit increased.
194-
* **Throttling**: With a large number of users and groups, you may encounter a `ThrottlingException` from the AWS SSO SCIM API. This project uses a [retryable HTTP client](https://github.com/p2p-b2b/httpretrier) to mitigate this, but it's still a possibility.
194+
* **Throttling**: With a large number of users and groups, you may encounter a `ThrottlingException` from the AWS SSO SCIM API. This project uses the [httpx](https://github.com/slashdevops/httpx) library with automatic retry and jitter backoff to mitigate this, but it's still a possibility.
195195
* **User Status**: The Google Workspace API doesn't differentiate between normal and guest users except for their status. This project only syncs `ACTIVE` users.
196196

197197
## For `ssosync` Users

cmd/idpscimcli/cmd/aws.go

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ package cmd
33
import (
44
"context"
55
"log/slog"
6-
"net/http"
76
"time"
87

9-
"github.com/p2p-b2b/httpretrier"
8+
"github.com/slashdevops/httpx"
109
"github.com/slashdevops/idp-scim-sync/internal/version"
1110
"github.com/slashdevops/idp-scim-sync/pkg/aws"
1211
"github.com/spf13/cobra"
@@ -92,11 +91,12 @@ func runAWSServiceConfig(_ *cobra.Command, _ []string) error {
9291
ctx, cancel := context.WithTimeout(context.Background(), reqTimeout)
9392
defer cancel()
9493

95-
httpRetryClient := httpretrier.NewClient(
96-
10, // Max Retries
97-
httpretrier.ExponentialBackoff(10*time.Millisecond, 500*time.Millisecond),
98-
nil, // Use http.DefaultTransport
99-
)
94+
httpRetryClient := httpx.NewClientBuilder().
95+
WithMaxRetries(10).
96+
WithRetryStrategy(httpx.JitterBackoffStrategy).
97+
WithRetryBaseDelay(500 * time.Millisecond).
98+
WithRetryMaxDelay(10 * time.Second).
99+
Build()
100100

101101
awsSCIMService, err := aws.NewSCIMService(httpRetryClient, cfg.AWSSCIMEndpoint, cfg.AWSSCIMAccessToken)
102102
if err != nil {
@@ -120,16 +120,14 @@ func runAWSGroupsList(_ *cobra.Command, _ []string) error {
120120
ctx, cancel := context.WithTimeout(context.Background(), reqTimeout)
121121
defer cancel()
122122

123-
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
124-
httpTransport.MaxIdleConns = 100
125-
httpTransport.MaxConnsPerHost = 100
126-
httpTransport.MaxIdleConnsPerHost = 100
127-
128-
httpClient := httpretrier.NewClient(
129-
10, // Max Retries
130-
httpretrier.ExponentialBackoff(10*time.Millisecond, 500*time.Millisecond),
131-
httpTransport,
132-
)
123+
httpClient := httpx.NewClientBuilder().
124+
WithMaxRetries(10).
125+
WithRetryStrategy(httpx.JitterBackoffStrategy).
126+
WithRetryBaseDelay(500 * time.Millisecond).
127+
WithRetryMaxDelay(10 * time.Second).
128+
WithMaxIdleConns(100).
129+
WithMaxIdleConnsPerHost(100).
130+
Build()
133131

134132
awsSCIMService, err := aws.NewSCIMService(httpClient, cfg.AWSSCIMEndpoint, cfg.AWSSCIMAccessToken)
135133
if err != nil {
@@ -154,16 +152,14 @@ func runAWSUsersList(_ *cobra.Command, _ []string) error {
154152
ctx, cancel := context.WithTimeout(context.Background(), reqTimeout)
155153
defer cancel()
156154

157-
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
158-
httpTransport.MaxIdleConns = 100
159-
httpTransport.MaxConnsPerHost = 100
160-
httpTransport.MaxIdleConnsPerHost = 100
161-
162-
httpClient := httpretrier.NewClient(
163-
10, // Max Retries
164-
httpretrier.ExponentialBackoff(10*time.Millisecond, 500*time.Millisecond),
165-
httpTransport,
166-
)
155+
httpClient := httpx.NewClientBuilder().
156+
WithMaxRetries(10).
157+
WithRetryStrategy(httpx.JitterBackoffStrategy).
158+
WithRetryBaseDelay(500 * time.Millisecond).
159+
WithRetryMaxDelay(10 * time.Second).
160+
WithMaxIdleConns(100).
161+
WithMaxIdleConnsPerHost(100).
162+
Build()
167163

168164
awsSCIMService, err := aws.NewSCIMService(httpClient, cfg.AWSSCIMEndpoint, cfg.AWSSCIMAccessToken)
169165
if err != nil {

cmd/idpscimcli/cmd/gws.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"os"
88
"time"
99

10-
"github.com/p2p-b2b/httpretrier"
10+
"github.com/slashdevops/httpx"
1111
"github.com/slashdevops/idp-scim-sync/internal/config"
1212
"github.com/slashdevops/idp-scim-sync/internal/version"
1313
"github.com/slashdevops/idp-scim-sync/pkg/google"
@@ -126,11 +126,12 @@ func getGWSDirectoryService(ctx context.Context) *google.DirectoryService {
126126
"https://www.googleapis.com/auth/admin.directory.user.readonly",
127127
}
128128

129-
httpRetryClient := httpretrier.NewClient(
130-
3, // Max Retries
131-
httpretrier.ExponentialBackoff(10*time.Millisecond, 100*time.Millisecond),
132-
nil, // Use http.DefaultTransport
133-
)
129+
httpRetryClient := httpx.NewClientBuilder().
130+
WithMaxRetries(3).
131+
WithRetryStrategy(httpx.ExponentialBackoffStrategy).
132+
WithRetryBaseDelay(500 * time.Millisecond).
133+
WithRetryMaxDelay(10 * time.Second).
134+
Build()
134135

135136
gServiceConfig := google.DirectoryServiceConfig{
136137
UserEmail: cfg.GWSUserEmail,

docs/Whats-New.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
This document tracks notable changes, new features, and bug fixes across releases.
44

5+
## v0.40.1
6+
7+
### Improved HTTP Retry Library
8+
9+
Replaced the `httpretrier` library with [httpx](https://github.com/slashdevops/httpx), a zero-dependency HTTP client with built-in retry support.
10+
11+
**Why:** The previous library did not properly handle HTTP `429 Too Many Requests` responses, which caused issues with AWS SSO SCIM API throttling under high load.
12+
13+
**What changed:**
14+
15+
* The `httpx` library automatically retries on `429` and `5xx` responses with configurable backoff strategies.
16+
* AWS SCIM API calls now use **jitter backoff** instead of simple exponential backoff, reducing the chance of thundering herd effects during rate limiting.
17+
* Google Workspace API calls use **exponential backoff** for reliable retries.
18+
* The `httpx` library has zero external dependencies and integrates with Go's `slog` logging.
19+
520
## v0.44.0
621

722
### Configurable User Fields

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@ require (
77
github.com/aws/aws-sdk-go-v2 v1.41.5
88
github.com/aws/aws-sdk-go-v2/config v1.32.13
99
github.com/aws/aws-sdk-go-v2/credentials v1.19.13
10-
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3
10+
github.com/aws/aws-sdk-go-v2/service/s3 v1.98.0
1111
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.5
1212
github.com/google/go-cmp v0.7.0
13-
github.com/p2p-b2b/httpretrier v0.0.4
1413
github.com/pkg/errors v0.9.1
14+
github.com/slashdevops/httpx v0.0.4
1515
github.com/spf13/cobra v1.10.2
1616
github.com/spf13/viper v1.21.0
1717
github.com/stretchr/testify v1.11.1
1818
go.uber.org/mock v0.6.0
1919
golang.org/x/oauth2 v0.36.0
2020
golang.org/x/sync v0.20.0
21-
google.golang.org/api v0.273.0
21+
google.golang.org/api v0.273.1
2222
gopkg.in/yaml.v3 v3.0.1
2323
)
2424

go.sum

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3x
3232
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
3333
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
3434
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
35-
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo=
36-
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
35+
github.com/aws/aws-sdk-go-v2/service/s3 v1.98.0 h1:foqo/ocQ7WqKwy3FojGtZQJo0FR4vto9qnz9VaumbCo=
36+
github.com/aws/aws-sdk-go-v2/service/s3 v1.98.0/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
3737
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.5 h1:z2ayoK3pOvf8ODj/vPR0FgAS5ONruBq0F94SRoW/BIU=
3838
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.5/go.mod h1:mpZB5HAl4ZIISod9qCi12xZ170TbHX9CCJV5y7nb7QU=
3939
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg=
@@ -82,8 +82,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
8282
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
8383
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
8484
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
85-
github.com/p2p-b2b/httpretrier v0.0.4 h1:TREDrCthpm0jMTLq2T9+rf8hhH9h36W8Vf8l3L5+WiI=
86-
github.com/p2p-b2b/httpretrier v0.0.4/go.mod h1:0yKtg4EekYBRzwLIiRVBqh4A/I3VQRiSdBnpu5InyOw=
8785
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
8886
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
8987
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -95,6 +93,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
9593
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
9694
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
9795
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
96+
github.com/slashdevops/httpx v0.0.4 h1:XhwMl9asfiPSqBaTv4wNjwFus6RFfgsEffl33BZazK4=
97+
github.com/slashdevops/httpx v0.0.4/go.mod h1:RIXp0/ylq/BqDlPo8glvay29Nr6zHw6jbExJneLYXKQ=
9898
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
9999
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
100100
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
@@ -146,8 +146,8 @@ golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
146146
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
147147
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
148148
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
149-
google.golang.org/api v0.273.0 h1:r/Bcv36Xa/te1ugaN1kdJ5LoA5Wj/cL+a4gj6FiPBjQ=
150-
google.golang.org/api v0.273.0/go.mod h1:JbAt7mF+XVmWu6xNP8/+CTiGH30ofmCmk9nM8d8fHew=
149+
google.golang.org/api v0.273.1 h1:L7G/TmpAMz0nKx/ciAVssVmWQiOF6+pOuXeKrWVsquY=
150+
google.golang.org/api v0.273.1/go.mod h1:JbAt7mF+XVmWu6xNP8/+CTiGH30ofmCmk9nM8d8fHew=
151151
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE=
152152
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw=
153153
google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5 h1:CogIeEXn4qWYzzQU0QqvYBM8yDF9cFYzDq9ojSpv0Js=

internal/setup/setup.go

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import (
1111

1212
"github.com/aws/aws-sdk-go-v2/service/s3"
1313
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
14-
"github.com/p2p-b2b/httpretrier"
1514
"github.com/pkg/errors"
15+
"github.com/slashdevops/httpx"
1616
"github.com/slashdevops/idp-scim-sync/internal/config"
1717
"github.com/slashdevops/idp-scim-sync/internal/core"
1818
"github.com/slashdevops/idp-scim-sync/internal/idp"
@@ -218,11 +218,12 @@ func SyncService(ctx context.Context, cfg *config.Config) (*core.SyncService, er
218218
gwsServiceAccountContent = gwsServiceAccount
219219
}
220220

221-
idpClient := httpretrier.NewClient(
222-
10, // Max Retries
223-
httpretrier.ExponentialBackoff(10*time.Millisecond, 500*time.Millisecond),
224-
nil, // Use http.DefaultTransport
225-
)
221+
idpClient := httpx.NewClientBuilder().
222+
WithMaxRetries(10).
223+
WithRetryStrategy(httpx.ExponentialBackoffStrategy).
224+
WithRetryBaseDelay(500 * time.Millisecond).
225+
WithRetryMaxDelay(10 * time.Second).
226+
Build()
226227

227228
userAgent := fmt.Sprintf("idp-scim-sync/%s", version.Version)
228229

@@ -257,12 +258,13 @@ func SyncService(ctx context.Context, cfg *config.Config) (*core.SyncService, er
257258

258259
// AWS SCIM Service
259260

260-
// httpClient
261-
scimClient := httpretrier.NewClient(
262-
10, // Max Retries
263-
httpretrier.ExponentialBackoff(10*time.Millisecond, 500*time.Millisecond),
264-
nil, // Use http.DefaultTransport
265-
)
261+
// httpClient with jitter backoff to avoid thundering herd on 429 rate limits
262+
scimClient := httpx.NewClientBuilder().
263+
WithMaxRetries(10).
264+
WithRetryStrategy(httpx.JitterBackoffStrategy).
265+
WithRetryBaseDelay(500 * time.Millisecond).
266+
WithRetryMaxDelay(10 * time.Second).
267+
Build()
266268

267269
awsSCIM, err := aws.NewSCIMService(scimClient, cfg.AWSSCIMEndpoint, cfg.AWSSCIMAccessToken)
268270
if err != nil {

0 commit comments

Comments
 (0)