GoのHTTPライブラリにRestyというものがある。しばしばこのライブラリを使用する機会があるため、調べたことを備忘録的に記述する。そのため網羅的な内容ではない。
RestyでHTTPリクエスト送信のリトライ処理を行う
何度失敗すれば諦めていいのか、これは人生にとって重要な問題だ。1度の失敗で諦めていいのか、もしくは無限に諦めず挑戦しつづけてもいいのか、これを判断するのはとても難しい。この難しい問題を考えるため、今回はRestyのリトライ処理の実装方法を確認する。
基本的なリトライの仕方は全て https://github.com/go-resty/resty に記載されている。
ダミーサーバーを実装する
まずはリクエストを送信するためのダミーサーバーを実装する。
URL設計
ダミーサーバーはURLによって、それぞれ異なるHTTPレスポンスを返すことにする。検証として使用するための以下のURLを実装する。
- GET /succes 成功
- GET /error HTTPレスポンスコードとしてInternal Server Errorを返す
- GET /success-but-fail HTTPレスポンスコードとしてOKを返すが、
{"status": "failed"}
というBODYを返す。
実装
package main
import (
"log"
"net/http"
)
func main () {
http.HandleFunc("/success", func (w http.ResponseWriter, r *http.Request) {
log.Print("Receive /success")
w.Header().Set("Content-Type","text/plain")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`Success!!`))
})
http.HandleFunc("/error", func (w http.ResponseWriter, r *http.Request) {
log.Print("Receive /error")
w.Header().Set("Content-Type","text/plain")
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`Error!!`))
})
http.HandleFunc("/success-but-fail", func (w http.ResponseWriter, r *http.Request) {
log.Print("Receive /success-but-fail")
w.Header().Set("Content-Type","application/json")
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"status": "failed"}`))
})
if err := http.ListenAndServe(":8000", nil); err != nil {
log.Panic("ListenAndServe:", err)
}
}
リクエストが受けとれることを確認する
package main
import (
"fmt"
"github.com/go-resty/resty/v2"
)
func main () {
resp, err := resty.New().R().Get("http://localhost:8000/success")
if err != nil {
panic(err)
}
fmt.Println(resp)
}
リトライ処理を実装する
HTTPステータスによってリトライする
リトライする条件はAddRetryCondition関数で登録できる。この関数がtrueを返すとリトライが実行される。この実装ではレスポンスのステータスコードが200でなければ、1秒間隔で最大3回のリクエストを送信する。
package main
import (
"fmt"
"time"
"github.com/go-resty/resty/v2"
)
func main () {
c := resty.New()
resp, err := c.SetRetryCount(2).
SetRetryWaitTime(1 * time.Second).
AddRetryCondition(func (r *resty.Response, err error) bool {
return r.StatusCode() != 200
}).
R().
Get("http://localhost:8000/error")
if err != nil {
panic(err)
}
fmt.Println(resp.StatusCode())
}
HTTPレスポンスのBODYによってリトライする
リトライする条件で扱えるものはレスポンスのステータスコードだけではなく、 他のさまざまな値を利用できる。今度はレスポンスボディの値を元にリトライ処理を実行する。
Web APIによっては、そのWeb API呼び出しが成功しているかどうかを示す方法としてHTTPレスポンスのステータスコードを用いることもあれば、ステータスコードは200 OKを返しレスポンスのエラーコードを含むJSONをボディ部として返すということもある。
今回は後者を想定し、レスポンスのボディ部のJSONのstatus属性がfailedであればリクエストの送信をリトライさせる。つまり {"status": "failed"}
というJSONが返されたらリトライする。
package main
import (
"fmt"
"time"
"encoding/json"
"github.com/go-resty/resty/v2"
)
type ResponseBody struct {
Status string `json:"status,omitempty"`
}
func main () {
c := resty.New()
b := &ResponseBody{}
resp, err := c.SetRetryCount(2).
SetRetryWaitTime(1 * time.Second).
AddRetryCondition(func (r *resty.Response, err error) bool {
json.Unmarshal(r.Body(), b)
return b.Status == "failed"
}).
R().
Get("http://localhost:8000/success-but-fail")
if err != nil {
panic(err)
}
fmt.Println(resp.StatusCode())
fmt.Println(b.Status)
}
Exponential Backoff
よくあるリクエストのリトライ処理のパターンとして Exponential Backoff
という方法がある。リトライするたびに次のリクエストを送信するまでの時間を長くするという方法だ。たとえば1回目のリトライを2秒後、2回目のリトライを4秒後、3回目のリトライを8秒後といったように、指数関数的に待ち時間を増やしていく。RestyではデフォルトのリトライのロジックにExponential Backoffアルゴリズムを採用している。
そのためリトライ回数やリトライの最大秒数などの値の設定だけでよい。
package main
import (
"fmt"
"time"
"github.com/go-resty/resty/v2"
)
func main () {
c := resty.New()
resp, err := c.SetRetryCount(5).
SetRetryWaitTime(1 * time.Second).
SetRetryMaxWaitTime(20 * time.Second).
AddRetryCondition(func (r *resty.Response, err error) bool {
return r.StatusCode() != 200
}).
R().
Get("http://localhost:8000/error")
if err != nil {
panic(err)
}
fmt.Println(resp.StatusCode())
}
Go Modules
go.mod::
module example
go 1.17
require github.com/go-resty/resty/v2 v2.7.0
require golang.org/x/net v0.7.0 // indirect
go.sum::
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
プロキシを中継してRestyでのリクエスト送信を行う
RestyはHTTP_PROXY環境変数やHTTPS_PROXY環境変数が設定されていれば、設定 されているプロキシにリクエストを中継させる。
export HTTP_PROXY=http://localhost:8080
export HTTPS_PROXY=http://localhost:8080
package main
import (
"fmt"
"github.com/go-resty/resty/v2"
)
func main() {
c := resty.New()
resp, err := c.R().Get("https://example.com")
fmt.Println(err)
fmt.Println(resp)
}
他にクライアントにSetProxyで設定する方法もある。
https://github.com/go-resty/resty#proxy-settings—client-as-well-as-at-request-level
各種HTTPレスポンスステータス時のRestyの挙動を確認する
RestyでHTTPリクエストを送信した時、レスポンスステータスによってどのような挙動になるのかを確認する。ダミーサーバーにはmitmproxyを使用する。
from mitmproxy import http
def request(flow: http.HTTPFlow) -> None:
flow.response = http.Response.make(503, content=b"foo")
次のように起動する。
mitmproxy -s _main.py
http.Response.make
の第1引数を変更することでステータスを変更する。
クライアントはシンプルなGETリクエストを送信し、そのerrを標準出力に表示する。
**
おおまかにHTTPステータスをざっと確認したが、HTTP的な問題が発生しない場合は err
には nil
が返される。 3xx
は非nilが返されているが、 3xx系に必要なレスポンスヘッダーが無いため、HTTPとして不正だから非nilになっているのだろう。
Pythonでは2xx系以外のレスポンスの場合に例外を送出するライブラリやメソッドが用意されていることもあるが、restyの場合はそうではない。そのため次のように愚直に返却されたものをチェックする必要がある。
- HTTP的なエラーが発生していないか?
- HTTPレスポンスのステータスコードは期待値か?
- HTTPレスポンスのヘッダーやボディは期待通りか?
様々なクライアントライブラリを触っていると、この当たりの挙動を思い出せず曖昧なまま実装し、結果として甘いエラー処理になりがちになる。きちんと挙動を調べ、確実な事を積み重ねようと思う。
まとめ
要求に応じて適切なリトライ処理を実装しよう。エラー時の挙動を確認しよう。