Compare commits
1 Commits
darabonba2
..
v2.x
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ac445bc69 |
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"scope": "darabonba",
|
||||
"name": "HttpClient",
|
||||
"version": "0.0.1",
|
||||
"main": "./main.dara",
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Alibaba Cloud SDK",
|
||||
"email": "sdk-team@alibabacloud.com"
|
||||
}
|
||||
],
|
||||
"libraries": {
|
||||
},
|
||||
"releases": {
|
||||
"go": "github.com/alibabacloud-go/tea/tea:v1.2.3-0.20240528133401-8788f78f9c46"
|
||||
},
|
||||
"go": {
|
||||
"interface": true,
|
||||
"clientName": "HttpClient",
|
||||
"typedef": {
|
||||
"HttpRequest": {
|
||||
"import": "net/http",
|
||||
"type": "http.Request"
|
||||
},
|
||||
"HttpResponse": {
|
||||
"import": "net/http",
|
||||
"type": "http.Response"
|
||||
},
|
||||
"HttpTransport": {
|
||||
"import": "net/http",
|
||||
"type": "http.Transport"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
typedef HttpRequest;
|
||||
typedef HttpResponse;
|
||||
typedef HttpTransport;
|
||||
|
||||
init(){
|
||||
}
|
||||
|
||||
async function call(request: HttpRequest, transport: HttpTransport): HttpResponse;
|
||||
@@ -1,4 +1,4 @@
|
||||
module github.com/alibabacloud-go/tea
|
||||
module github.com/alibabacloud-go/tea/v2
|
||||
|
||||
go 1.14
|
||||
|
||||
@@ -6,5 +6,5 @@ require (
|
||||
github.com/alibabacloud-go/debug v1.0.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/modern-go/reflect2 v1.0.2
|
||||
golang.org/x/net v0.22.0
|
||||
golang.org/x/net v0.20.0
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
github.com/alibabacloud-go/debug v1.0.0 h1:3eIEQWfay1fB24PQIEzXAswlVJtdQok8f3EVN5VrBnA=
|
||||
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
@@ -9,14 +10,15 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
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/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -24,9 +26,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
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/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -37,14 +38,12 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
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/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/alibabacloud-go/tea/utils"
|
||||
"github.com/alibabacloud-go/tea/v2/utils"
|
||||
)
|
||||
|
||||
func TestBackoffPolicy(t *testing.T) {
|
||||
|
||||
+21
-55
@@ -22,35 +22,14 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/alibabacloud-go/debug/debug"
|
||||
"github.com/alibabacloud-go/tea/utils"
|
||||
"github.com/alibabacloud-go/tea/v2/utils"
|
||||
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
var debugLog = debug.Init("tea")
|
||||
|
||||
type HttpRequest interface {
|
||||
}
|
||||
|
||||
type HttpResponse interface {
|
||||
}
|
||||
|
||||
type HttpClient interface {
|
||||
Call(request *http.Request, transport *http.Transport) (response *http.Response, err error)
|
||||
}
|
||||
|
||||
type teaClient struct {
|
||||
sync.Mutex
|
||||
httpClient *http.Client
|
||||
ifInit bool
|
||||
}
|
||||
|
||||
func (client *teaClient) Call(request *http.Request, transport *http.Transport) (response *http.Response, err error) {
|
||||
response, err = client.httpClient.Do(request)
|
||||
return
|
||||
}
|
||||
|
||||
var hookDo = func(fn func(req *http.Request, transport *http.Transport) (*http.Response, error)) func(req *http.Request, transport *http.Transport) (*http.Response, error) {
|
||||
var hookDo = func(fn func(req *http.Request) (*http.Response, error)) func(req *http.Request) (*http.Response, error) {
|
||||
return fn
|
||||
}
|
||||
|
||||
@@ -100,7 +79,12 @@ type RuntimeObject struct {
|
||||
Tracker *utils.ReaderTracker `json:"tracker" xml:"tracker"`
|
||||
Logger *utils.Logger `json:"logger" xml:"logger"`
|
||||
RetryOptions *RetryOptions `json:"retryOptions" xml:"retryOptions"`
|
||||
HttpClient
|
||||
}
|
||||
|
||||
type teaClient struct {
|
||||
sync.Mutex
|
||||
httpClient *http.Client
|
||||
ifInit bool
|
||||
}
|
||||
|
||||
var clientPool = &sync.Map{}
|
||||
@@ -144,9 +128,6 @@ func NewRuntimeObject(runtime map[string]interface{}) *RuntimeObject {
|
||||
if runtime["logger"] != nil {
|
||||
runtimeObject.Logger = runtime["logger"].(*utils.Logger)
|
||||
}
|
||||
if runtime["httpClient"] != nil {
|
||||
runtimeObject.HttpClient = runtime["httpClient"].(HttpClient)
|
||||
}
|
||||
return runtimeObject
|
||||
}
|
||||
|
||||
@@ -198,8 +179,8 @@ func getTeaClient(tag string) *teaClient {
|
||||
return client.(*teaClient)
|
||||
}
|
||||
|
||||
// DoAction is used send request to server
|
||||
func DoAction(request *Request, runtimeObject *RuntimeObject) (response *Response, err error) {
|
||||
// DoRequest is used send request to server
|
||||
func DoRequest(request *Request, runtimeObject *RuntimeObject) (response *Response, err error) {
|
||||
if runtimeObject == nil {
|
||||
runtimeObject = &RuntimeObject{}
|
||||
}
|
||||
@@ -247,27 +228,19 @@ func DoAction(request *Request, runtimeObject *RuntimeObject) (response *Respons
|
||||
return
|
||||
}
|
||||
httpRequest.Host = StringValue(request.Domain)
|
||||
var client HttpClient
|
||||
if runtimeObject.HttpClient == nil {
|
||||
client = getTeaClient(runtimeObject.getClientTag(StringValue(request.Domain)))
|
||||
} else {
|
||||
client = runtimeObject.HttpClient
|
||||
}
|
||||
|
||||
trans, err := getHttpTransport(request, runtimeObject)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if defaultClient, ok := client.(*teaClient); ok {
|
||||
defaultClient.Lock()
|
||||
if !defaultClient.ifInit || defaultClient.httpClient.Transport == nil {
|
||||
defaultClient.httpClient.Transport = trans
|
||||
client := getTeaClient(runtimeObject.getClientTag(StringValue(request.Domain)))
|
||||
client.Lock()
|
||||
if !client.ifInit {
|
||||
trans, err := getHttpTransport(request, runtimeObject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defaultClient.httpClient.Timeout = time.Duration(IntValue(runtimeObject.ReadTimeout)) * time.Millisecond
|
||||
defaultClient.ifInit = true
|
||||
defaultClient.Unlock()
|
||||
client.httpClient.Timeout = time.Duration(IntValue(runtimeObject.ReadTimeout)) * time.Millisecond
|
||||
client.httpClient.Transport = trans
|
||||
client.ifInit = true
|
||||
}
|
||||
|
||||
client.Unlock()
|
||||
for key, value := range request.Headers {
|
||||
if value == nil || key == "content-length" {
|
||||
continue
|
||||
@@ -289,7 +262,7 @@ func DoAction(request *Request, runtimeObject *RuntimeObject) (response *Respons
|
||||
putMsgToMap(fieldMap, httpRequest)
|
||||
startTime := time.Now()
|
||||
fieldMap["{start_time}"] = startTime.Format("2006-01-02 15:04:05")
|
||||
res, err := hookDo(client.Call)(httpRequest, trans)
|
||||
res, err := hookDo(client.httpClient.Do)(httpRequest)
|
||||
fieldMap["{cost}"] = time.Since(startTime).String()
|
||||
completedBytes := int64(0)
|
||||
if runtimeObject.Tracker != nil {
|
||||
@@ -317,13 +290,6 @@ func DoAction(request *Request, runtimeObject *RuntimeObject) (response *Respons
|
||||
return
|
||||
}
|
||||
|
||||
// DoRequest is used send request to server
|
||||
func DoRequest(request *Request, requestRuntime map[string]interface{}) (response *Response, err error) {
|
||||
runtimeObject := NewRuntimeObject(requestRuntime)
|
||||
response, err = DoAction(request, runtimeObject)
|
||||
return
|
||||
}
|
||||
|
||||
func getHttpTransport(req *Request, runtime *RuntimeObject) (*http.Transport, error) {
|
||||
trans := new(http.Transport)
|
||||
httpProxy, err := getHttpProxy(StringValue(req.Protocol), StringValue(req.Domain), runtime)
|
||||
|
||||
+24
-131
@@ -15,7 +15,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alibabacloud-go/tea/utils"
|
||||
"github.com/alibabacloud-go/tea/v2/utils"
|
||||
)
|
||||
|
||||
type test struct {
|
||||
@@ -267,8 +267,8 @@ DEMilhlFY+o9mqCygFVxuvHtQVhpPS938H2h7/P6pXN65jK2Y5hHefZEELq9ulQe
|
||||
func Test_DoRequest(t *testing.T) {
|
||||
origTestHookDo := hookDo
|
||||
defer func() { hookDo = origTestHookDo }()
|
||||
hookDo = func(fn func(req *http.Request, transport *http.Transport) (*http.Response, error)) func(req *http.Request, transport *http.Transport) (*http.Response, error) {
|
||||
return func(req *http.Request, transport *http.Transport) (*http.Response, error) {
|
||||
hookDo = func(fn func(req *http.Request) (*http.Response, error)) func(req *http.Request) (*http.Response, error) {
|
||||
return func(req *http.Request) (*http.Response, error) {
|
||||
return mockResponse(200, ``, errors.New("Internal error"))
|
||||
}
|
||||
}
|
||||
@@ -278,113 +278,6 @@ func Test_DoRequest(t *testing.T) {
|
||||
utils.AssertNil(t, resp)
|
||||
utils.AssertEqual(t, `net/http: invalid method "TEA TEST"`, err.Error())
|
||||
|
||||
request.Method = String("")
|
||||
request.Protocol = String("https")
|
||||
request.Query = map[string]*string{
|
||||
"tea": String("test"),
|
||||
}
|
||||
runtimeObj["httpsProxy"] = "# #%gfdf"
|
||||
resp, err = DoRequest(request, runtimeObj)
|
||||
utils.AssertNil(t, resp)
|
||||
utils.AssertContains(t, err.Error(), `invalid URL escape "%gf"`)
|
||||
|
||||
request.Pathname = String("?log")
|
||||
request.Headers["tea"] = String("")
|
||||
request.Headers["content-length"] = nil
|
||||
runtimeObj["httpsProxy"] = "http://someuser:somepassword@ecs.aliyun.com"
|
||||
resp, err = DoRequest(request, runtimeObj)
|
||||
utils.AssertNil(t, resp)
|
||||
utils.AssertEqual(t, `Internal error`, err.Error())
|
||||
|
||||
request.Headers["host"] = String("tea-cn-hangzhou.aliyuncs.com:80")
|
||||
request.Headers["user-agent"] = String("test")
|
||||
resp, err = DoRequest(request, runtimeObj)
|
||||
utils.AssertNil(t, resp)
|
||||
utils.AssertEqual(t, `Internal error`, err.Error())
|
||||
|
||||
runtimeObj["socks5Proxy"] = "# #%gfdf"
|
||||
resp, err = DoRequest(request, runtimeObj)
|
||||
utils.AssertNil(t, resp)
|
||||
utils.AssertContains(t, err.Error(), ` invalid URL escape "%gf"`)
|
||||
|
||||
hookDo = func(fn func(req *http.Request, transport *http.Transport) (*http.Response, error)) func(req *http.Request, transport *http.Transport) (*http.Response, error) {
|
||||
return func(req *http.Request, transport *http.Transport) (*http.Response, error) {
|
||||
return mockResponse(200, ``, nil)
|
||||
}
|
||||
}
|
||||
runtimeObj["socks5Proxy"] = "socks5://someuser:somepassword@ecs.aliyun.com"
|
||||
runtimeObj["localAddr"] = "127.0.0.1"
|
||||
resp, err = DoRequest(request, runtimeObj)
|
||||
utils.AssertNil(t, err)
|
||||
utils.AssertEqual(t, "test", StringValue(resp.Headers["tea"]))
|
||||
|
||||
runtimeObj["key"] = "private rsa key"
|
||||
runtimeObj["cert"] = "private certification"
|
||||
runtimeObj["ca"] = "private ca"
|
||||
runtimeObj["ignoreSSL"] = true
|
||||
resp, err = DoRequest(request, runtimeObj)
|
||||
utils.AssertNil(t, err)
|
||||
utils.AssertNotNil(t, resp)
|
||||
|
||||
// update the host is to restart a client
|
||||
request.Headers["host"] = String("a.com")
|
||||
runtimeObj["ignoreSSL"] = false
|
||||
resp, err = DoRequest(request, runtimeObj)
|
||||
utils.AssertNotNil(t, err)
|
||||
utils.AssertEqual(t, "tls: failed to find any PEM data in certificate input", err.Error())
|
||||
utils.AssertNil(t, resp)
|
||||
|
||||
// update the host is to restart a client
|
||||
request.Headers["host"] = String("b.com")
|
||||
runtimeObj["key"] = key
|
||||
runtimeObj["cert"] = cert
|
||||
runtimeObj["ca"] = "private ca"
|
||||
_, err = DoRequest(request, runtimeObj)
|
||||
utils.AssertNotNil(t, err)
|
||||
utils.AssertEqual(t, "Failed to parse root certificate", err.Error())
|
||||
|
||||
// update the host is to restart a client
|
||||
request.Headers["host"] = String("c.com")
|
||||
runtimeObj["ca"] = ca
|
||||
resp, err = DoRequest(request, runtimeObj)
|
||||
utils.AssertNil(t, err)
|
||||
utils.AssertEqual(t, "test", StringValue(resp.Headers["tea"]))
|
||||
|
||||
request.Protocol = String("HTTP")
|
||||
runtimeObj["ignoreSSL"] = false
|
||||
resp, err = DoRequest(request, runtimeObj)
|
||||
utils.AssertNil(t, err)
|
||||
utils.AssertEqual(t, "test", StringValue(resp.Headers["tea"]))
|
||||
|
||||
hookDo = func(fn func(req *http.Request, transport *http.Transport) (*http.Response, error)) func(req *http.Request, transport *http.Transport) (*http.Response, error) {
|
||||
return func(req *http.Request, transport *http.Transport) (*http.Response, error) {
|
||||
utils.AssertEqual(t, "tea-cn-hangzhou.aliyuncs.com:1080", req.Host)
|
||||
return mockResponse(200, ``, errors.New("Internal error"))
|
||||
}
|
||||
}
|
||||
request.Pathname = String("/log")
|
||||
request.Protocol = String("http")
|
||||
request.Port = Int(1080)
|
||||
request.Headers["host"] = String("tea-cn-hangzhou.aliyuncs.com")
|
||||
resp, err = DoRequest(request, runtimeObj)
|
||||
utils.AssertNil(t, resp)
|
||||
utils.AssertEqual(t, `Internal error`, err.Error())
|
||||
}
|
||||
|
||||
func Test_DoAction(t *testing.T) {
|
||||
origTestHookDo := hookDo
|
||||
defer func() { hookDo = origTestHookDo }()
|
||||
hookDo = func(fn func(req *http.Request, transport *http.Transport) (*http.Response, error)) func(req *http.Request, transport *http.Transport) (*http.Response, error) {
|
||||
return func(req *http.Request, transport *http.Transport) (*http.Response, error) {
|
||||
return mockResponse(200, ``, errors.New("Internal error"))
|
||||
}
|
||||
}
|
||||
request := NewRequest()
|
||||
request.Method = String("TEA TEST")
|
||||
resp, err := DoAction(request, nil)
|
||||
utils.AssertNil(t, resp)
|
||||
utils.AssertEqual(t, `net/http: invalid method "TEA TEST"`, err.Error())
|
||||
|
||||
request.Method = String("")
|
||||
request.Protocol = String("https")
|
||||
request.Query = map[string]*string{
|
||||
@@ -392,7 +285,7 @@ func Test_DoAction(t *testing.T) {
|
||||
}
|
||||
runtimeObj["httpsProxy"] = "# #%gfdf"
|
||||
runtimeObject := NewRuntimeObject(runtimeObj)
|
||||
resp, err = DoAction(request, runtimeObject)
|
||||
resp, err = DoRequest(request, runtimeObject)
|
||||
utils.AssertNil(t, resp)
|
||||
utils.AssertContains(t, err.Error(), `invalid URL escape "%gf"`)
|
||||
|
||||
@@ -401,32 +294,32 @@ func Test_DoAction(t *testing.T) {
|
||||
request.Headers["content-length"] = nil
|
||||
runtimeObj["httpsProxy"] = "http://someuser:somepassword@ecs.aliyun.com"
|
||||
runtimeObject = NewRuntimeObject(runtimeObj)
|
||||
resp, err = DoAction(request, runtimeObject)
|
||||
resp, err = DoRequest(request, runtimeObject)
|
||||
utils.AssertNil(t, resp)
|
||||
utils.AssertEqual(t, `Internal error`, err.Error())
|
||||
|
||||
request.Headers["host"] = String("tea-cn-hangzhou.aliyuncs.com:80")
|
||||
request.Headers["user-agent"] = String("test")
|
||||
runtimeObject = NewRuntimeObject(runtimeObj)
|
||||
resp, err = DoAction(request, runtimeObject)
|
||||
resp, err = DoRequest(request, runtimeObject)
|
||||
utils.AssertNil(t, resp)
|
||||
utils.AssertEqual(t, `Internal error`, err.Error())
|
||||
|
||||
runtimeObj["socks5Proxy"] = "# #%gfdf"
|
||||
runtimeObject = NewRuntimeObject(runtimeObj)
|
||||
resp, err = DoAction(request, runtimeObject)
|
||||
resp, err = DoRequest(request, runtimeObject)
|
||||
utils.AssertNil(t, resp)
|
||||
utils.AssertContains(t, err.Error(), ` invalid URL escape "%gf"`)
|
||||
|
||||
hookDo = func(fn func(req *http.Request, transport *http.Transport) (*http.Response, error)) func(req *http.Request, transport *http.Transport) (*http.Response, error) {
|
||||
return func(req *http.Request, transport *http.Transport) (*http.Response, error) {
|
||||
hookDo = func(fn func(req *http.Request) (*http.Response, error)) func(req *http.Request) (*http.Response, error) {
|
||||
return func(req *http.Request) (*http.Response, error) {
|
||||
return mockResponse(200, ``, nil)
|
||||
}
|
||||
}
|
||||
runtimeObj["socks5Proxy"] = "socks5://someuser:somepassword@ecs.aliyun.com"
|
||||
runtimeObj["localAddr"] = "127.0.0.1"
|
||||
runtimeObject = NewRuntimeObject(runtimeObj)
|
||||
resp, err = DoAction(request, runtimeObject)
|
||||
resp, err = DoRequest(request, runtimeObject)
|
||||
utils.AssertNil(t, err)
|
||||
utils.AssertEqual(t, "test", StringValue(resp.Headers["tea"]))
|
||||
|
||||
@@ -435,7 +328,7 @@ func Test_DoAction(t *testing.T) {
|
||||
runtimeObj["ca"] = "private ca"
|
||||
runtimeObj["ignoreSSL"] = true
|
||||
runtimeObject = NewRuntimeObject(runtimeObj)
|
||||
resp, err = DoAction(request, runtimeObject)
|
||||
resp, err = DoRequest(request, runtimeObject)
|
||||
utils.AssertNil(t, err)
|
||||
utils.AssertNotNil(t, resp)
|
||||
|
||||
@@ -443,7 +336,7 @@ func Test_DoAction(t *testing.T) {
|
||||
request.Headers["host"] = String("a.com")
|
||||
runtimeObj["ignoreSSL"] = false
|
||||
runtimeObject = NewRuntimeObject(runtimeObj)
|
||||
resp, err = DoAction(request, runtimeObject)
|
||||
resp, err = DoRequest(request, runtimeObject)
|
||||
utils.AssertNotNil(t, err)
|
||||
utils.AssertEqual(t, "tls: failed to find any PEM data in certificate input", err.Error())
|
||||
utils.AssertNil(t, resp)
|
||||
@@ -454,7 +347,7 @@ func Test_DoAction(t *testing.T) {
|
||||
runtimeObj["cert"] = cert
|
||||
runtimeObj["ca"] = "private ca"
|
||||
runtimeObject = NewRuntimeObject(runtimeObj)
|
||||
_, err = DoAction(request, runtimeObject)
|
||||
_, err = DoRequest(request, runtimeObject)
|
||||
utils.AssertNotNil(t, err)
|
||||
utils.AssertEqual(t, "Failed to parse root certificate", err.Error())
|
||||
|
||||
@@ -462,19 +355,19 @@ func Test_DoAction(t *testing.T) {
|
||||
request.Headers["host"] = String("c.com")
|
||||
runtimeObj["ca"] = ca
|
||||
runtimeObject = NewRuntimeObject(runtimeObj)
|
||||
resp, err = DoAction(request, runtimeObject)
|
||||
resp, err = DoRequest(request, runtimeObject)
|
||||
utils.AssertNil(t, err)
|
||||
utils.AssertEqual(t, "test", StringValue(resp.Headers["tea"]))
|
||||
|
||||
request.Protocol = String("HTTP")
|
||||
runtimeObj["ignoreSSL"] = false
|
||||
runtimeObject = NewRuntimeObject(runtimeObj)
|
||||
resp, err = DoAction(request, runtimeObject)
|
||||
resp, err = DoRequest(request, runtimeObject)
|
||||
utils.AssertNil(t, err)
|
||||
utils.AssertEqual(t, "test", StringValue(resp.Headers["tea"]))
|
||||
|
||||
hookDo = func(fn func(req *http.Request, transport *http.Transport) (*http.Response, error)) func(req *http.Request, transport *http.Transport) (*http.Response, error) {
|
||||
return func(req *http.Request, transport *http.Transport) (*http.Response, error) {
|
||||
hookDo = func(fn func(req *http.Request) (*http.Response, error)) func(req *http.Request) (*http.Response, error) {
|
||||
return func(req *http.Request) (*http.Response, error) {
|
||||
utils.AssertEqual(t, "tea-cn-hangzhou.aliyuncs.com:1080", req.Host)
|
||||
return mockResponse(200, ``, errors.New("Internal error"))
|
||||
}
|
||||
@@ -484,16 +377,16 @@ func Test_DoAction(t *testing.T) {
|
||||
request.Port = Int(1080)
|
||||
request.Headers["host"] = String("tea-cn-hangzhou.aliyuncs.com")
|
||||
runtimeObject = NewRuntimeObject(runtimeObj)
|
||||
resp, err = DoAction(request, runtimeObject)
|
||||
resp, err = DoRequest(request, runtimeObject)
|
||||
utils.AssertNil(t, resp)
|
||||
utils.AssertEqual(t, `Internal error`, err.Error())
|
||||
}
|
||||
|
||||
func Test_DoActionWithConcurrent(t *testing.T) {
|
||||
func Test_DoRequestWithConcurrent(t *testing.T) {
|
||||
origTestHookDo := hookDo
|
||||
defer func() { hookDo = origTestHookDo }()
|
||||
hookDo = func(fn func(req *http.Request, transport *http.Transport) (*http.Response, error)) func(req *http.Request, transport *http.Transport) (*http.Response, error) {
|
||||
return func(req *http.Request, transport *http.Transport) (*http.Response, error) {
|
||||
hookDo = func(fn func(req *http.Request) (*http.Response, error)) func(req *http.Request) (*http.Response, error) {
|
||||
return func(req *http.Request) (*http.Response, error) {
|
||||
return mockResponse(200, ``, nil)
|
||||
}
|
||||
}
|
||||
@@ -509,7 +402,7 @@ func Test_DoActionWithConcurrent(t *testing.T) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
request := NewRequest()
|
||||
resp, err := DoAction(request, runtimeObject)
|
||||
resp, err := DoRequest(request, runtimeObject)
|
||||
utils.AssertNil(t, err)
|
||||
utils.AssertNotNil(t, resp)
|
||||
wg.Done()
|
||||
@@ -590,11 +483,11 @@ func Test_SetDialContext(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_hookdo(t *testing.T) {
|
||||
fn := func(req *http.Request, transport *http.Transport) (*http.Response, error) {
|
||||
fn := func(req *http.Request) (*http.Response, error) {
|
||||
return nil, errors.New("hookdo")
|
||||
}
|
||||
result := hookDo(fn)
|
||||
resp, err := result(nil, nil)
|
||||
resp, err := result(nil)
|
||||
utils.AssertNil(t, resp)
|
||||
utils.AssertEqual(t, "hookdo", err.Error())
|
||||
}
|
||||
|
||||
+11
-56
@@ -13,11 +13,11 @@ type BaseError interface {
|
||||
error
|
||||
ErrorName() *string
|
||||
ErrorCode() *string
|
||||
RetryAfterTimeMillis() *int64
|
||||
}
|
||||
|
||||
// CastError is used for cast type fails
|
||||
type CastError struct {
|
||||
BaseError
|
||||
Message *string
|
||||
Code *string
|
||||
}
|
||||
@@ -43,10 +43,12 @@ func (err *CastError) ErrorCode() *string {
|
||||
return err.Code
|
||||
}
|
||||
|
||||
// SDKError struct is used save error code and message, use as ResponseError
|
||||
func (err *CastError) RetryAfterTimeMillis() *int64 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SDKError struct is used save error code and message
|
||||
type SDKError struct {
|
||||
BaseError
|
||||
Name string
|
||||
Code *string
|
||||
StatusCode *int
|
||||
Message *string
|
||||
@@ -55,9 +57,6 @@ type SDKError struct {
|
||||
errMsg *string
|
||||
Description *string
|
||||
AccessDeniedDetail map[string]interface{}
|
||||
RequestId *string
|
||||
Retryable *bool
|
||||
RetryAfter *int64
|
||||
}
|
||||
|
||||
// NewSDKError is used for shortly create SDKError object
|
||||
@@ -126,54 +125,6 @@ func NewSDKError(obj map[string]interface{}) *SDKError {
|
||||
return err
|
||||
}
|
||||
|
||||
func NewError(name string, obj map[string]interface{}) *SDKError {
|
||||
err := &SDKError{
|
||||
Name: name,
|
||||
}
|
||||
if val, ok := obj["code"].(int); ok {
|
||||
err.Code = String(strconv.Itoa(val))
|
||||
} else if val, ok := obj["code"].(string); ok {
|
||||
err.Code = String(val)
|
||||
}
|
||||
if obj["message"] != nil {
|
||||
err.Message = String(obj["message"].(string))
|
||||
}
|
||||
if obj["description"] != nil {
|
||||
err.Description = String(obj["description"].(string))
|
||||
}
|
||||
if detail := obj["accessDeniedDetail"]; detail != nil {
|
||||
r := reflect.ValueOf(detail)
|
||||
if r.Kind().String() == "map" {
|
||||
res := make(map[string]interface{})
|
||||
tmp := r.MapKeys()
|
||||
for _, key := range tmp {
|
||||
res[key.String()] = r.MapIndex(key).Interface()
|
||||
}
|
||||
err.AccessDeniedDetail = res
|
||||
}
|
||||
}
|
||||
if obj["statusCode"] != nil {
|
||||
err.StatusCode = Int(obj["statusCode"].(int))
|
||||
}
|
||||
if obj["requestId"] != nil {
|
||||
err.RequestId = String(obj["requestId"].(string))
|
||||
}
|
||||
if obj["retryable"] != nil {
|
||||
err.Retryable = Bool(obj["retryable"].(bool))
|
||||
}
|
||||
if obj["retryAfter"] != nil {
|
||||
err.RetryAfter = Int64(obj["retryAfter"].(int64))
|
||||
}
|
||||
if data := obj["data"]; data != nil {
|
||||
byt := bytes.NewBuffer([]byte{})
|
||||
jsonEncoder := json.NewEncoder(byt)
|
||||
jsonEncoder.SetEscapeHTML(false)
|
||||
jsonEncoder.Encode(data)
|
||||
err.Data = String(string(bytes.TrimSpace(byt.Bytes())))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Set ErrMsg by msg
|
||||
func (err *SDKError) SetErrMsg(msg string) {
|
||||
err.errMsg = String(msg)
|
||||
@@ -189,9 +140,13 @@ func (err *SDKError) Error() string {
|
||||
}
|
||||
|
||||
func (err *SDKError) ErrorName() *string {
|
||||
return String(err.Name)
|
||||
return String("SDKError")
|
||||
}
|
||||
|
||||
func (err *SDKError) ErrorCode() *string {
|
||||
return err.Code
|
||||
}
|
||||
|
||||
func (err *SDKError) RetryAfterTimeMillis() *int64 {
|
||||
return nil
|
||||
}
|
||||
|
||||
+4
-38
@@ -3,7 +3,7 @@ package tea
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/alibabacloud-go/tea/utils"
|
||||
"github.com/alibabacloud-go/tea/v2/utils"
|
||||
)
|
||||
|
||||
func TestCastError(t *testing.T) {
|
||||
@@ -12,6 +12,7 @@ func TestCastError(t *testing.T) {
|
||||
utils.AssertEqual(t, "cast error", err.Error())
|
||||
utils.AssertEqual(t, "", StringValue(err.ErrorCode()))
|
||||
utils.AssertEqual(t, "CastError", StringValue(err.ErrorName()))
|
||||
utils.AssertNil(t, err.RetryAfterTimeMillis())
|
||||
}
|
||||
|
||||
func TestSDKError(t *testing.T) {
|
||||
@@ -45,6 +46,7 @@ func TestSDKError(t *testing.T) {
|
||||
utils.AssertEqual(t, "description", *err.Description)
|
||||
utils.AssertEqual(t, "ImplicitDeny", err.AccessDeniedDetail["NoPermissionType"])
|
||||
utils.AssertEqual(t, 123, err.AccessDeniedDetail["UserId"])
|
||||
utils.AssertNil(t, err.RetryAfterTimeMillis())
|
||||
|
||||
err = NewSDKError(map[string]interface{}{
|
||||
"statusCode": "404",
|
||||
@@ -109,41 +111,5 @@ func TestSDKErrorCode404(t *testing.T) {
|
||||
utils.AssertNotNil(t, err)
|
||||
utils.AssertEqual(t, "SDKError:\n StatusCode: 404\n Code: NOTFOUND\n Message: message\n Data: \n", err.Error())
|
||||
utils.AssertEqual(t, "NOTFOUND", StringValue(err.ErrorCode()))
|
||||
utils.AssertEqual(t, "", StringValue(err.ErrorName()))
|
||||
}
|
||||
|
||||
func TestNewError(t *testing.T) {
|
||||
var err0 BaseError
|
||||
err0 = NewError("ResponseError", map[string]interface{}{
|
||||
"code": "code",
|
||||
"statusCode": 404,
|
||||
"message": "message",
|
||||
"data": map[string]interface{}{
|
||||
"httpCode": "404",
|
||||
"requestId": "dfadfa32cgfdcasd4313",
|
||||
"hostId": "github.com/alibabacloud/tea",
|
||||
"recommend": "https://中文?q=a.b&product=c&requestId=123",
|
||||
},
|
||||
"description": "description",
|
||||
"accessDeniedDetail": map[string]interface{}{
|
||||
"AuthAction": "ram:ListUsers",
|
||||
"AuthPrincipalType": "SubUser",
|
||||
"PolicyType": "ResourceGroupLevelIdentityBassdPolicy",
|
||||
"NoPermissionType": "ImplicitDeny",
|
||||
"UserId": 123,
|
||||
},
|
||||
"requestId": "123456",
|
||||
"retryable": true,
|
||||
"retryAfter": int64(100),
|
||||
})
|
||||
utils.AssertNotNil(t, err0)
|
||||
utils.AssertEqual(t, "SDKError:\n StatusCode: 404\n Code: code\n Message: message\n Data: {\"hostId\":\"github.com/alibabacloud/tea\",\"httpCode\":\"404\",\"recommend\":\"https://中文?q=a.b&product=c&requestId=123\",\"requestId\":\"dfadfa32cgfdcasd4313\"}\n", err0.Error())
|
||||
var err *SDKError
|
||||
err = err0.(*SDKError)
|
||||
err.SetErrMsg("test")
|
||||
utils.AssertEqual(t, "test", err.Error())
|
||||
utils.AssertEqual(t, 404, *err.StatusCode)
|
||||
utils.AssertEqual(t, "description", *err.Description)
|
||||
utils.AssertEqual(t, "ImplicitDeny", err.AccessDeniedDetail["NoPermissionType"])
|
||||
utils.AssertEqual(t, 123, err.AccessDeniedDetail["UserId"])
|
||||
utils.AssertEqual(t, "SDKError", StringValue(err.ErrorName()))
|
||||
}
|
||||
|
||||
+1
-6
@@ -14,12 +14,7 @@ func Prettify(i interface{}) string {
|
||||
return string(resp)
|
||||
}
|
||||
|
||||
func Sleep(backoffTime *int) {
|
||||
sleeptime := time.Duration(IntValue(backoffTime)) * time.Second
|
||||
time.Sleep(sleeptime)
|
||||
}
|
||||
|
||||
func SleepMillis(backoffTime *int64) {
|
||||
func Sleep(backoffTime *int64) {
|
||||
sleeptime := time.Duration(Int64Value(backoffTime)) * time.Millisecond
|
||||
time.Sleep(sleeptime)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alibabacloud-go/tea/utils"
|
||||
"github.com/alibabacloud-go/tea/v2/utils"
|
||||
)
|
||||
|
||||
type PrettifyTest struct {
|
||||
@@ -32,11 +32,11 @@ func Test_Prettify(t *testing.T) {
|
||||
utils.AssertEqual(t, str, "null")
|
||||
}
|
||||
|
||||
func Test_SleepMillis(t *testing.T) {
|
||||
func Test_Sleep(t *testing.T) {
|
||||
start := time.Now()
|
||||
SleepMillis(Int64(1000))
|
||||
SleepMillis(Int64(0))
|
||||
SleepMillis(nil)
|
||||
Sleep(Int64(1000))
|
||||
Sleep(Int64(0))
|
||||
Sleep(nil)
|
||||
cost := time.Since(start)
|
||||
utils.AssertEqual(t, cost.Seconds() >= 1, true)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/alibabacloud-go/tea/utils"
|
||||
"github.com/alibabacloud-go/tea/v2/utils"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/modern-go/reflect2"
|
||||
)
|
||||
|
||||
+3
-54
@@ -2,10 +2,9 @@ package tea
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/alibabacloud-go/tea/utils"
|
||||
"github.com/alibabacloud-go/tea/v2/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -66,11 +65,6 @@ func ShouldRetry(options *RetryOptions, ctx *RetryPolicyContext) *bool {
|
||||
if IntValue(ctx.RetriesAttempted) > IntValue(retryCondition.MaxAttempts) {
|
||||
return Bool(false)
|
||||
}
|
||||
if err1, ok := err.(*SDKError); ok {
|
||||
if BoolValue(err1.Retryable) == false {
|
||||
return Bool(false)
|
||||
}
|
||||
}
|
||||
return Bool(true)
|
||||
}
|
||||
}
|
||||
@@ -97,13 +91,9 @@ func GetBackoffDelay(options *RetryOptions, ctx *RetryPolicyContext) *int64 {
|
||||
} else {
|
||||
maxDelay = DefaultMaxDelay.Milliseconds()
|
||||
}
|
||||
|
||||
if err1, ok := err.(*SDKError); ok {
|
||||
if err1.RetryAfter != nil {
|
||||
return Int64(int64(math.Min(float64(Int64Value(err1.RetryAfter)), float64(maxDelay))))
|
||||
}
|
||||
if err.RetryAfterTimeMillis() != nil {
|
||||
return Int64(int64(math.Min(float64(Int64Value(err.RetryAfterTimeMillis())), float64(maxDelay))))
|
||||
}
|
||||
|
||||
if retryCondition.Backoff == nil {
|
||||
return Int64(DefaultMinDelay.Milliseconds())
|
||||
}
|
||||
@@ -115,44 +105,3 @@ func GetBackoffDelay(options *RetryOptions, ctx *RetryPolicyContext) *int64 {
|
||||
}
|
||||
return Int64(DefaultMinDelay.Milliseconds())
|
||||
}
|
||||
|
||||
// Deperacated
|
||||
func AllowRetry(retry interface{}, retryTimes *int) *bool {
|
||||
if IntValue(retryTimes) == 0 {
|
||||
return Bool(true)
|
||||
}
|
||||
retryMap, ok := retry.(map[string]interface{})
|
||||
if !ok {
|
||||
return Bool(false)
|
||||
}
|
||||
retryable, ok := retryMap["retryable"].(bool)
|
||||
if !ok || !retryable {
|
||||
return Bool(false)
|
||||
}
|
||||
|
||||
maxAttempts, ok := retryMap["maxAttempts"].(int)
|
||||
if !ok || maxAttempts < IntValue(retryTimes) {
|
||||
return Bool(false)
|
||||
}
|
||||
return Bool(true)
|
||||
}
|
||||
|
||||
// Deperacated
|
||||
func GetBackoffTime(backoff interface{}, retrytimes *int) *int {
|
||||
backoffMap, ok := backoff.(map[string]interface{})
|
||||
if !ok {
|
||||
return Int(0)
|
||||
}
|
||||
policy, ok := backoffMap["policy"].(string)
|
||||
if !ok || policy == "no" {
|
||||
return Int(0)
|
||||
}
|
||||
|
||||
period, ok := backoffMap["period"].(int)
|
||||
if !ok || period == 0 {
|
||||
return Int(0)
|
||||
}
|
||||
|
||||
maxTime := math.Pow(2.0, float64(IntValue(retrytimes)))
|
||||
return Int(rand.Intn(int(maxTime-1)) * period)
|
||||
}
|
||||
|
||||
+73
-129
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/alibabacloud-go/tea/utils"
|
||||
"github.com/alibabacloud-go/tea/v2/utils"
|
||||
)
|
||||
|
||||
func Test_Trans(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user