Skip to content

Commit 4bb0498

Browse files
authored
feat: add more retry options (#169)
Introduced retry options for GraphQL clients, allowing customization for retries, backoff intervals, and error handling during GraphQL operations
1 parent f20a101 commit 4bb0498

28 files changed

+1621
-619
lines changed

.github/workflows/lint.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@ jobs:
2828
- name: Vet
2929
run: go vet ./...
3030
- name: Lint
31-
uses: golangci/golangci-lint-action@v6
31+
uses: golangci/golangci-lint-action@v7
3232
with:
3333
version: latest
3434
only-new-issues: true
35-
skip-cache: true
3635
args: --timeout=120s

.github/workflows/test.yml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,6 @@ jobs:
1010
test-go:
1111
name: Run unit and integration tests
1212
runs-on: ubuntu-latest
13-
permissions:
14-
pull-requests: write
15-
# Required: allow read access to the content for analysis.
16-
contents: read
17-
# Optional: Allow write access to checks to allow the action to annotate code in the PR.
18-
checks: write
1913
steps:
2014
- name: Checkout
2115
uses: actions/checkout@v4

.golangci.yml

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
version: "2"
2+
3+
linters:
4+
default: all
5+
disable:
6+
- err113
7+
- lll
8+
- depguard
9+
- wrapcheck
10+
- varnamelen
11+
- exhaustruct
12+
- ireturn
13+
- gochecknoglobals
14+
- mnd
15+
- recvcheck
16+
- wsl
17+
- makezero
18+
- godox
19+
- gocognit
20+
- gocyclo
21+
- funcorder
22+
- forcetypeassert
23+
- containedctx
24+
- maintidx
25+
settings:
26+
exhaustive:
27+
# Program elements to check for exhaustiveness.
28+
check:
29+
- switch
30+
- map
31+
# Presence of "default" case in switch statements satisfies exhaustiveness,
32+
# even if all enum members are not listed.
33+
default-signifies-exhaustive: true
34+
cyclop:
35+
max-complexity: 70
36+
revive:
37+
# Maximum number of open files at the same time.
38+
max-open-files: 2048
39+
severity: error
40+
rules:
41+
- name: var-naming
42+
disabled: true
43+
- name: exported
44+
disabled: true
45+
gosec:
46+
excludes:
47+
- G115
48+
- G301
49+
- G304
50+
- G306
51+
- G404
52+
funlen:
53+
lines: 210
54+
statements: 120
55+
nestif:
56+
min-complexity: 10
57+
58+
# Defines a set of rules to ignore issues.
59+
# It does not skip the analysis, and so does not ignore "typecheck" errors.
60+
exclusions:
61+
# Mode of the generated files analysis.
62+
#
63+
# - `strict`: sources are excluded by strictly following the Go generated file convention.
64+
# Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$`
65+
# This line must appear before the first non-comment, non-blank text in the file.
66+
# https://go.dev/s/generatedcode
67+
# - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc.
68+
# - `disable`: disable the generated files exclusion.
69+
#
70+
# Default: lax
71+
generated: strict
72+
# Log a warning if an exclusion rule is unused.
73+
warn-unused: false
74+
rules: []
75+
# - path: connector/api/
76+
# linters:
77+
# - godot
78+
# Which file paths to exclude: they will be analyzed, but issues from them won't be reported.
79+
# "/" will be replaced by the current OS file path separator to properly work on Windows.
80+
paths:
81+
- ".*_test\\.go$"
82+
- example/
83+
84+
formatters:
85+
# Enable specific formatter.
86+
# Default: [] (uses standard Go formatting)
87+
enable:
88+
- gci
89+
- gofmt
90+
- gofumpt
91+
- goimports
92+
- golines
93+
# Formatters settings.
94+
settings: {}
95+
exclusions:
96+
# Mode of the generated files analysis.
97+
#
98+
# - `strict`: sources are excluded by strictly following the Go generated file convention.
99+
# Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$`
100+
# This line must appear before the first non-comment, non-blank text in the file.
101+
# https://go.dev/s/generatedcode
102+
# - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc.
103+
# - `disable`: disable the generated files exclusion.
104+
#
105+
# Default: lax
106+
generated: strict
107+
# Which file paths to exclude.
108+
# Default: []
109+
paths: []

README.md

Lines changed: 61 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,48 +8,47 @@ The subscription client follows Apollo client specification https://github.com/a
88

99
Package `graphql` provides a GraphQL client implementation.
1010

11-
For more information, see package [`github.com/shurcooL/githubv4`](https://github.com/shurcooL/githubv4), which is a specialized version targeting GitHub GraphQL API v4. That package is driving the feature development.
12-
1311
**Note**: Before v0.8.0, `QueryRaw`, `MutateRaw`, and `Subscribe` methods return `*json.RawMessage`. This output type is redundant to be decoded. From v0.8.0, the output type is changed to `[]byte`.
1412

1513
- [go-graphql-client](#go-graphql-client)
16-
- [Installation](#installation)
17-
- [Usage](#usage)
18-
- [Authentication](#authentication)
19-
- [WithRequestModifier](#withrequestmodifier)
20-
- [OAuth2](#oauth2)
21-
- [Simple Query](#simple-query)
22-
- [Arguments and Variables](#arguments-and-variables)
23-
- [Custom scalar tag](#custom-scalar-tag)
24-
- [Skip GraphQL field](#skip-graphql-field)
25-
- [Inline Fragments](#inline-fragments)
26-
- [Specify GraphQL type name](#specify-graphql-type-name)
27-
- [Mutations](#mutations)
28-
- [Mutations Without Fields](#mutations-without-fields)
29-
- [Subscription](#subscription)
30-
- [Usage](#usage-1)
31-
- [Subscribe](#subscribe)
32-
- [Stop the subscription](#stop-the-subscription)
33-
- [Authentication](#authentication-1)
34-
- [Options](#options)
35-
- [Subscription Protocols](#subscription-protocols)
36-
- [Handle connection error](#handle-connection-error)
37-
- [Connection Initialisation Timeout](#connection-initialisation-timeout)
38-
- [WebSocket Connection Idle Timeout](#websocket-connection-idle-timeout)
39-
- [Events](#events)
40-
- [Custom HTTP Client](#custom-http-client)
41-
- [Custom WebSocket client](#custom-websocket-client)
42-
- [Options](#options-1)
43-
- [Execute pre-built query](#execute-pre-built-query)
44-
- [Get extensions from response](#get-extensions-from-response)
45-
- [Get headers from response](#get-headers-from-response)
46-
- [With operation name (deprecated)](#with-operation-name-deprecated)
47-
- [Raw bytes response](#raw-bytes-response)
48-
- [Multiple mutations with ordered map](#multiple-mutations-with-ordered-map)
49-
- [Debugging and Unit test](#debugging-and-unit-test)
50-
- [Directories](#directories)
51-
- [References](#references)
52-
- [License](#license)
14+
- [Installation](#installation)
15+
- [Usage](#usage)
16+
- [Authentication](#authentication)
17+
- [WithRequestModifier](#withrequestmodifier)
18+
- [OAuth2](#oauth2)
19+
- [Simple Query](#simple-query)
20+
- [Arguments and Variables](#arguments-and-variables)
21+
- [Custom scalar tag](#custom-scalar-tag)
22+
- [Skip GraphQL field](#skip-graphql-field)
23+
- [Inline Fragments](#inline-fragments)
24+
- [Specify GraphQL type name](#specify-graphql-type-name)
25+
- [Mutations](#mutations)
26+
- [Mutations Without Fields](#mutations-without-fields)
27+
- [Retry Options](#retry-options)
28+
- [Subscription](#subscription)
29+
- [Usage](#usage-1)
30+
- [Subscribe](#subscribe)
31+
- [Stop the subscription](#stop-the-subscription)
32+
- [Authentication](#authentication-1)
33+
- [Options](#options)
34+
- [Subscription Protocols](#subscription-protocols)
35+
- [Handle connection error](#handle-connection-error)
36+
- [Connection Initialisation Timeout](#connection-initialisation-timeout)
37+
- [WebSocket Connection Idle Timeout](#websocket-connection-idle-timeout)
38+
- [Events](#events)
39+
- [Custom HTTP Client](#custom-http-client)
40+
- [Custom WebSocket client](#custom-websocket-client)
41+
- [Options](#options-1)
42+
- [Execute pre-built query](#execute-pre-built-query)
43+
- [Get extensions from response](#get-extensions-from-response)
44+
- [Get headers from response](#get-headers-from-response)
45+
- [With operation name (deprecated)](#with-operation-name-deprecated)
46+
- [Raw bytes response](#raw-bytes-response)
47+
- [Multiple mutations with ordered map](#multiple-mutations-with-ordered-map)
48+
- [Debugging and Unit test](#debugging-and-unit-test)
49+
- [Directories](#directories)
50+
- [References](#references)
51+
- [License](#license)
5352

5453
## Installation
5554

@@ -490,6 +489,29 @@ fmt.Printf("Created a review: %s.\n", m.CreateReview)
490489
// Created a review: .
491490
```
492491
492+
### Retry Options
493+
494+
Construct the client with the following options:
495+
496+
```go
497+
client := graphql.NewClient("/graphql", http.DefaultClient,
498+
// number of retries
499+
graphql.WithRetry(3),
500+
// base backoff interval. Optional, default 1 second.
501+
// Prioritize the Retry-After header if it exists in the response.
502+
graphql.WithRetryBaseDelay(time.Second),
503+
// exponential rate. Optional, default 2.0
504+
graphql.WithRetryExponentialRate(2),
505+
// retry on http statuses. Optional, default: 429, 502, 503, 504
506+
graphql.WithRetryHTTPStatus([]int{http.StatusServiceUnavailable}),
507+
// if the http status is 200 but the graphql response is error,
508+
// use this option to check if the error is retryable.
509+
graphql.WithRetryOnGraphQLError(func(errs graphql.Errors) bool {
510+
return len(errs) == 1 && errs[0].Message == "Field 'user' is missing required arguments: login"
511+
}),
512+
)
513+
```
514+
493515
### Subscription
494516
495517
#### Usage

example/graphql-ws-bc/client/main.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ import (
1515

1616
func main() {
1717
protocol := graphql.GraphQLWS
18-
protocolArg := flag.String("protocol", "graphql-ws", "The protocol is used for the subscription")
18+
protocolArg := flag.String(
19+
"protocol",
20+
"graphql-ws",
21+
"The protocol is used for the subscription",
22+
)
1923
flag.Parse()
2024

2125
if protocolArg != nil {
@@ -64,7 +68,6 @@ func startSubscription(protocol graphql.SubscriptionProtocolType) error {
6468
}
6569

6670
_, err := client.Subscribe(sub, nil, func(data []byte, err error) error {
67-
6871
if err != nil {
6972
log.Println(err)
7073
return nil
@@ -76,7 +79,6 @@ func startSubscription(protocol graphql.SubscriptionProtocolType) error {
7679
log.Printf("hello: %+v", string(data))
7780
return nil
7881
})
79-
8082
if err != nil {
8183
panic(err)
8284
}

example/hasura/client/graphql-ws/client.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ func main() {
2626
}
2727

2828
func startSubscription() error {
29-
3029
client := graphql.NewSubscriptionClient(serverEndpoint).
3130
WithProtocol(graphql.GraphQLWS).
3231
WithConnectionParams(map[string]interface{}{
@@ -59,7 +58,6 @@ func startSubscription() error {
5958
}
6059

6160
_, err := client.Subscribe(sub, nil, func(data []byte, err error) error {
62-
6361
if err != nil {
6462
log.Println(err)
6563
return nil
@@ -71,7 +69,6 @@ func startSubscription() error {
7169
log.Println(string(data))
7270
return nil
7371
})
74-
7572
if err != nil {
7673
panic(err)
7774
}
@@ -89,7 +86,6 @@ type user_insert_input map[string]interface{}
8986

9087
// insertUsers insert users to the graphql server, so the subscription client can receive messages
9188
func insertUsers() {
92-
9389
client := graphql.NewClient(serverEndpoint, &http.Client{
9490
Transport: headerRoundTripper{
9591
setHeaders: func(req *http.Request) {
@@ -124,7 +120,12 @@ func insertUsers() {
124120
},
125121
},
126122
}
127-
err := client.Mutate(context.Background(), &q, variables, graphql.OperationName("InsertUser"))
123+
err := client.Mutate(
124+
context.Background(),
125+
&q,
126+
variables,
127+
graphql.OperationName("InsertUser"),
128+
)
128129
if err != nil {
129130
fmt.Println(err)
130131
}
@@ -133,7 +134,7 @@ func insertUsers() {
133134
}
134135

135136
func randomString() string {
136-
var letter = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
137+
letter := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
137138

138139
b := make([]rune, 16)
139140
for i := range b {

example/hasura/client/subscriptions-transport-ws/client.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ func main() {
2525
}
2626

2727
func startSubscription() error {
28-
2928
client := graphql.NewSubscriptionClient(serverEndpoint).
3029
WithConnectionParams(map[string]interface{}{
3130
"headers": map[string]string{
@@ -55,7 +54,6 @@ func startSubscription() error {
5554
}
5655

5756
subId, err := client.Subscribe(sub, nil, func(data []byte, err error) error {
58-
5957
if err != nil {
6058
log.Println(err)
6159
return nil
@@ -67,7 +65,6 @@ func startSubscription() error {
6765
log.Println(string(data))
6866
return nil
6967
})
70-
7168
if err != nil {
7269
panic(err)
7370
}
@@ -85,7 +82,6 @@ type user_insert_input map[string]interface{}
8582

8683
// insertUsers insert users to the graphql server, so the subscription client can receive messages
8784
func insertUsers() {
88-
8985
client := graphql.NewClient(serverEndpoint, &http.Client{
9086
Transport: headerRoundTripper{
9187
setHeaders: func(req *http.Request) {
@@ -120,7 +116,12 @@ func insertUsers() {
120116
},
121117
},
122118
}
123-
err := client.Mutate(context.Background(), &q, variables, graphql.OperationName("InsertUser"))
119+
err := client.Mutate(
120+
context.Background(),
121+
&q,
122+
variables,
123+
graphql.OperationName("InsertUser"),
124+
)
124125
if err != nil {
125126
fmt.Println(err)
126127
}
@@ -129,7 +130,7 @@ func insertUsers() {
129130
}
130131

131132
func randomString() string {
132-
var letter = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
133+
letter := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
133134

134135
b := make([]rune, 16)
135136
for i := range b {

example/realworld/main.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ func main() {
2020
}
2121

2222
func run() error {
23-
2423
url := "https://rickandmortyapi.com/graphql"
2524

2625
client := graphql.NewClient(url, nil)

0 commit comments

Comments
 (0)