diff --git a/client.go b/client.go new file mode 100644 index 0000000..04fd0ac --- /dev/null +++ b/client.go @@ -0,0 +1,263 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package easy_http + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strings" + "sync" + + "github.com/cloudwego/hertz/pkg/app/client" + "github.com/cloudwego/hertz/pkg/common/config" + "github.com/cloudwego/hertz/pkg/protocol" + "github.com/cloudwego/hertz/pkg/protocol/consts" +) + +type Client struct { + baseURL string + header http.Header + + beforeRequest []RequestMiddleware + afterResponse []ResponseMiddleware + afterResponseLock *sync.RWMutex + + enableDiscovery bool + + client *client.Client + options []config.ClientOption +} + +type ( + RequestMiddleware func(*Client, *Request) error + ResponseMiddleware func(*Client, *Response) error +) + +var ( + hdrContentTypeKey = http.CanonicalHeaderKey(consts.HeaderContentType) + hostHeader = "Host" + + plainTextType = consts.MIMETextPlainUTF8 + jsonContentType = consts.MIMEApplicationJSON + formContentType = consts.MIMEApplicationHTMLForm + formDataContentType = consts.MIMEMultipartPOSTForm +) + +// createClient creates a new client instance with configured request and response middleware. +// It accepts a client.Client pointer and optional config.ClientOption parameters. +// +// For Example: +// +// cc := &client.Client{} +// opts := []config.ClientOption{...} +// client := createClient(cc, opts...) +// +// Note: This function configures middleware for request processing and response parsing. +func createClient(cc *client.Client, opts ...config.ClientOption) *Client { + c := &Client{ + afterResponseLock: &sync.RWMutex{}, + + client: cc, + options: opts, + } + + c.beforeRequest = []RequestMiddleware{ + parseRequestURL, + parseRequestHeader, + createHTTPRequest, + } + + c.afterResponse = []ResponseMiddleware{ + parseResponseBody, + } + + return c +} + +// R initializes and returns a new Request instance. +// It sets up QueryParam, Header, PathParams, and RawRequest fields. +// +// For Example: +// +// client := &Client{} +// req := client.R() +// +// Note: This method does not take any parameters. +func (c *Client) R() *Request { + r := &Request{ + QueryParam: url.Values{}, + Header: http.Header{}, + PathParams: map[string]string{}, + RawRequest: &protocol.Request{}, + + client: c, + } + return r +} + +// EnableServiceDiscovery enables service discovery for the client. +// It sets the enableDiscovery field to true and returns the modified client. +// +// Example: +// +// client.EnableServiceDiscovery() +func (c *Client) EnableServiceDiscovery() *Client { + c.enableDiscovery = true + return c +} + +// UseMiddleware adds one or more middleware to the client's request processing chain. +// It returns the client instance for chaining. +// +// For Example: +// +// client.UseMiddleware(middleware1, middleware2) +func (c *Client) UseMiddleware(mws ...client.Middleware) *Client { + c.client.Use(mws...) + return c +} + +// AddHeader method adds a custom HTTP header to the Client instance. +// It accepts header name and value as parameters. +// +// For Example: +// +// client.AddHeader("Authorization", "Bearer token"). +// AddHeader("Content-Type", "application/json") +// +// Returns the updated Client instance for chaining. +func (c *Client) AddHeader(header, value string) *Client { + c.header.Add(header, value) + return c +} + +// AddHeaders adds multiple HTTP headers to the client instance. +// It iterates over the provided map and calls AddHeader for each key-value pair. +// +// For Example: +// +// client.AddHeaders(map[string]string{ +// "Authorization": "Bearer token", +// "Content-Type": "application/json", +// }) +// +// Returns the updated client instance. +func (c *Client) AddHeaders(headers map[string]string) *Client { + for k, v := range headers { + c.AddHeader(k, v) + } + return c +} + +// GetClient retrieves the underlying client.Client instance from the Client. +// It returns a pointer to the client.Client instance. +// +// For Example: +// +// client := &Client{client: &client.Client{}} +// underlyingClient := client.GetClient() +func (c *Client) GetClient() *client.Client { + return c.client +} + +// SetBaseURL sets the base URL for the client and trims trailing slashes. +// It returns the updated client instance. +// +// For Example: +// +// client.SetBaseURL("https://example.com/") +// +// Note: trailing slashes are removed from the URL. +func (c *Client) SetBaseURL(url string) *Client { + c.baseURL = strings.TrimRight(url, "/") + return c +} + +// SetServiceName sets the service name and updates the base URL. +// It formats the name as the base URL and updates the client. +// +// For Example: +// +// client.SetServiceName("example.com") +// +// Note: This method does not check the validity of the URL. +func (c *Client) SetServiceName(name string) *Client { + c.SetBaseURL(fmt.Sprintf("http://%s", name)) + return c +} + +// NewRequest creates a new Request instance. +// It calls the R method of the Client. +// +// For Example: +// +// req := client.NewRequest() +// +// Note: This method does not take any parameters. +func (c *Client) NewRequest() *Request { + return c.R() +} + +// execute method executes an HTTP request and processes the response. +// It locks the post-request hooks to prevent concurrency issues. +// +// req := &Request{} +// resp, err := client.execute(req) +// if err != nil { +// log.Fatalf("Request failed: %v", err) +// } +// +// Note: Handles request and response middleware, and sets Host header. +func (c *Client) execute(req *Request) (*Response, error) { + // Lock the post-request hooks. + c.afterResponseLock.RLock() + defer c.afterResponseLock.RUnlock() + // Apply Request middleware + var err error + for _, f := range c.beforeRequest { + if err = f(c, req); err != nil { + return nil, err + } + } + + if hostHeader := req.Header.Get(hostHeader); hostHeader != "" { + req.RawRequest.SetHost(hostHeader) + } + req.hasCreate = true + + resp := &protocol.Response{} + err = c.client.Do(context.Background(), req.RawRequest, resp) + response := &Response{ + Request: req, + RawResponse: resp, + } + + if err != nil { + return response, err + } + + // Apply Response middleware + for _, f := range c.afterResponse { + if err = f(c, response); err != nil { + break + } + } + + return response, err +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..de47549 --- /dev/null +++ b/client_test.go @@ -0,0 +1,66 @@ +package easy_http + +import ( + "context" + "fmt" + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/app/server" + "net/url" + "strings" + "testing" + "time" +) + +func runHertz(t *testing.T) { + hertz := server.Default(server.WithHostPorts("127.0.0.1:6666")) + hertz.GET("/test_query", func(c context.Context, ctx *app.RequestContext) { + type Query struct { + Q1 []string `query:"q1"` + Q2 string `query:"q2"` + Q3 string `query:"q3"` + } + var req Query + err := ctx.BindQuery(&req) + if err != nil { + t.Fatal(err) + } + if len(req.Q1) != 2 { + t.Errorf("expected q1 has 2 element, but get %d", len(req.Q1)) + } + if req.Q2 != "q2" { + t.Errorf("expected q2, but get %s", req.Q2) + } + if req.Q3 != "q3" { + t.Errorf("expected q3, but get %s", req.Q3) + } + ctx.JSON(200, map[string]string{ + "q1": strings.Join(req.Q1, ","), + "q2": req.Q2, + "q3": req.Q3, + }) + }) + + go hertz.Spin() + time.Sleep(100 * time.Millisecond) +} + +func TestSetQueryParam(t *testing.T) { + runHertz(t) + //c := MustNewClient().UseMiddleware().SetBaseURL("http://127.0.0.1:6666") + res := make(map[string]string) + value := url.Values{} + value.Set("q3", "q3") + resp, err := R(). + SetResult(res). + AddQueryParam("q1", "q1"). + AddQueryParams(map[string]string{"q2": "q2", "q1": "q11"}). + AddQueryParamsFromValues(value). + Get(context.Background(), "http://127.0.0.1:6666/test_query") + if err != nil { + t.Fatal(err) + } + fmt.Println(resp.BodyString()) + fmt.Println(resp.Result()) + fmt.Println(res) + +} diff --git a/easy_http.go b/easy_http.go new file mode 100644 index 0000000..8a4dfdc --- /dev/null +++ b/easy_http.go @@ -0,0 +1,59 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package easy_http + +import ( + "github.com/cloudwego/hertz/pkg/app/client" + "github.com/cloudwego/hertz/pkg/common/config" + "github.com/cloudwego/hertz/pkg/network/standard" +) + +// NewClient creates a new client instance supporting HTTPS. +// It accepts a variadic list of config.ClientOption for configuration. +// +// For Example: +// +// client, err := NewClient(config.WithDialer(customDialer)) +// if err != nil { +// log.Fatalf("Failed to create client: %v", err) +// } +// +// Note: Uses standard library dialer by default for HTTPS support. +func NewClient(opts ...config.ClientOption) (*Client, error) { + // use standard network library to support https by default + opts = append(opts, client.WithDialer(standard.NewDialer())) + c, err := client.NewClient(opts...) + return createClient(c, opts...), err +} + +// MustNewClient creates a new client instance with given options. +// It defaults to using the standard library for HTTPS support. +// +// For Example: +// +// client := MustNewClient(config.WithTimeout(5 * time.Second)) +// +// Note: It panics if client creation fails. +func MustNewClient(opts ...config.ClientOption) *Client { + // use standard network library to support https by default + opts = append(opts, client.WithDialer(standard.NewDialer())) + c, err := client.NewClient(opts...) + if err != nil { + panic(err) + } + return createClient(c, opts...) +} diff --git a/easy_http_default.go b/easy_http_default.go new file mode 100644 index 0000000..ad95cd9 --- /dev/null +++ b/easy_http_default.go @@ -0,0 +1,29 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package easy_http + +// R creates a new request instance. +// It uses MustNewClient to create a client and configure middleware. +// +// Example: +// +// req := R() +// +// Note: MustNewClient may panic if client creation fails. +func R() *Request { + return MustNewClient().NewRequest() +} diff --git a/easy_http_test.go b/easy_http_test.go new file mode 100644 index 0000000..a848917 --- /dev/null +++ b/easy_http_test.go @@ -0,0 +1 @@ +package easy_http diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..0d990df --- /dev/null +++ b/example/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "fmt" + + "github.com/cloudwego/hertz/pkg/app/client" + "github.com/hertz-contrib/easy_http" +) + +func main() { + opts1 := easy_http.NewOption().WithDialTimeout(10).WithWriteTimeout(10) + hertzClient, _ := client.NewClient(client.WithDialTimeout(10), client.WithWriteTimeout(10)) + opts2 := easy_http.NewOption().WithHertzRawOption(hertzClient) + + c1, _ := easy_http.NewClient(opts1) + c2 := easy_http.MustNewClient(opts2) + c3 := easy_http.MustNewClient(&easy_http.Option{}) + + res, err := c1.SetHeader("test", "test").SetQueryParam("test1", "test1").R().Get("http://www.baidu.com") + if err != nil { + panic(err) + } + fmt.Println(res) + + res, err = c2.SetHeader("test", "test").SetQueryParam("test1", "test1").R().Get("http://www.baidu.com") + if err != nil { + panic(err) + } + + res, err = c3.SetHeader("test", "test").SetQueryParam("test1", "test1").R().Get("http://www.baidu.com") + if err != nil { + panic(err) + } + + fmt.Println(res) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a5cb248 --- /dev/null +++ b/go.mod @@ -0,0 +1,35 @@ +module github.com/hertz-contrib/easy_http + +go 1.19 + +require ( + github.com/cloudwego/hertz v0.8.1 + github.com/li-jin-gou/http2curl v0.1.2 +) + +require ( + github.com/andybalholm/brotli v1.0.4 // indirect + github.com/bytedance/go-tagexpr/v2 v2.9.2 // indirect + github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7 // indirect + github.com/bytedance/sonic v1.8.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/cloudwego/netpoll v0.5.0 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/go-resty/resty/v2 v2.12.0 // indirect + github.com/golang/protobuf v1.5.0 // indirect + github.com/henrylee2cn/ameda v1.4.10 // indirect + github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8 // indirect + github.com/klauspost/compress v1.15.9 // indirect + github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/nyaruka/phonenumbers v1.0.55 // indirect + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.44.0 // indirect + golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/sys v0.18.0 // indirect + google.golang.org/protobuf v1.27.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f31ed6a --- /dev/null +++ b/go.sum @@ -0,0 +1,149 @@ +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/bytedance/go-tagexpr/v2 v2.9.2 h1:QySJaAIQgOEDQBLS3x9BxOWrnhqu5sQ+f6HaZIxD39I= +github.com/bytedance/go-tagexpr/v2 v2.9.2/go.mod h1:5qsx05dYOiUXOUgnQ7w3Oz8BYs2qtM/bJokdLb79wRM= +github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7 h1:PtwsQyQJGxf8iaPptPNaduEIu9BnrNms+pcRdHAxZaM= +github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q= +github.com/bytedance/mockey v1.2.1 h1:g84ngI88hz1DR4wZTL3yOuqlEcq67MretBfQUdXwrmw= +github.com/bytedance/mockey v1.2.1/go.mod h1:+Jm/fzWZAuhEDrPXVjDf/jLM2BlLXJkwk94zf2JZ3X4= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.8.1 h1:NqAHCaGaTzro0xMmnTCLUyRlbEP6r8MCA1cJUrH3Pu4= +github.com/bytedance/sonic v1.8.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/cloudwego/hertz v0.8.1 h1:3Upzd9o5yNPz6rLx70J5xpo5emosKNkmwW00WgQhf/0= +github.com/cloudwego/hertz v0.8.1/go.mod h1:WliNtVbwihWHHgAaIQEbVXl0O3aWj0ks1eoPrcEAnjs= +github.com/cloudwego/netpoll v0.5.0 h1:oRrOp58cPCvK2QbMozZNDESvrxQaEHW2dCimmwH1lcU= +github.com/cloudwego/netpoll v0.5.0/go.mod h1:xVefXptcyheopwNDZjDPcfU6kIjZXZ4nY550k1yH9eQ= +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/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/go-resty/resty/v2 v2.12.0 h1:rsVL8P90LFvkUYq/V5BTVe203WfRIU4gvcf+yfzJzGA= +github.com/go-resty/resty/v2 v2.12.0/go.mod h1:o0yGPrkS3lOe1+eFajk6kBW8ScXzwU3hD69/gt2yB/0= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/henrylee2cn/ameda v1.4.8/go.mod h1:liZulR8DgHxdK+MEwvZIylGnmcjzQ6N6f2PlWe7nEO4= +github.com/henrylee2cn/ameda v1.4.10 h1:JdvI2Ekq7tapdPsuhrc4CaFiqw6QXFvZIULWJgQyCAk= +github.com/henrylee2cn/ameda v1.4.10/go.mod h1:liZulR8DgHxdK+MEwvZIylGnmcjzQ6N6f2PlWe7nEO4= +github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8 h1:yE9ULgp02BhYIrO6sdV/FPe0xQM6fNHkVQW2IAymfM0= +github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8/go.mod h1:Nhe/DM3671a5udlv2AdV2ni/MZzgfv2qrPL5nIi3EGQ= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/li-jin-gou/http2curl v0.1.2 h1:5otsFvKP4y4ON7XXRjILpa/LEVKRpw/zYMu31/3Mbs0= +github.com/li-jin-gou/http2curl v0.1.2/go.mod h1:yGoThsrrVSWB+ShTAeudwEv2R5Dmp6CCI72eXQI9EIA= +github.com/nyaruka/phonenumbers v1.0.55 h1:bj0nTO88Y68KeUQ/n3Lo2KgK7lM1hF7L9NFuwcCl3yg= +github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U= +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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.44.0 h1:R+gLUhldIsfg1HokMuQjdQ5bh9nuXHPIfvkYUu9eR5Q= +github.com/valyala/fasthttp v1.44.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +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.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +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/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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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= +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-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/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.0.0-20220728004956-3c1f35247d10/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 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.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/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/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +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/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/middleware.go b/middleware.go new file mode 100644 index 0000000..eb93c64 --- /dev/null +++ b/middleware.go @@ -0,0 +1,316 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package easy_http + +import ( + "encoding/json" + "encoding/xml" + "fmt" + "io" + "net/http" + "net/url" + "reflect" + "regexp" + "strings" + + "github.com/cloudwego/hertz/pkg/protocol" + "github.com/cloudwego/hertz/pkg/protocol/consts" +) + +var ( + jsonCheck = regexp.MustCompile(`(?i:(application|text)/(json|.*\+json|json\-.*)(; |$))`) + xmlCheck = regexp.MustCompile(`(?i:(application|text)/(xml|.*\+xml)(; |$))`) +) + +// parseRequestURL parses and processes the request URL to ensure it is correctly formatted. +// It includes path and query parameters. +// +// For Example: +// +// client := &Client{baseURL: "http://example.com"} +// req := &Request{URL: "/api/:id", PathParams: map[string]string{"id": "123"}} +// err := parseRequestURL(client, req) +// +// Note: This function handles both path and query parameters. +func parseRequestURL(c *Client, r *Request) error { + if len(r.PathParams) > 0 { + for p, v := range r.PathParams { + if strings.HasSuffix(r.URL, "*"+p) { // "*" must be at end of route + r.URL = strings.Replace(r.URL, "*"+p, url.PathEscape(v), 1) + continue + } + r.URL = strings.Replace(r.URL, ":"+p, url.PathEscape(v), -1) + } + } + + // Parsing request URL + reqURL, err := url.Parse(r.URL) + if err != nil { + return err + } + + // If Request.URL is relative path then added c.HostURL into + // the request URL otherwise Request.URL will be used as-is + if !reqURL.IsAbs() { + r.URL = reqURL.String() + if len(r.URL) > 0 && r.URL[0] != '/' { + r.URL = "/" + r.URL + } + reqURL, err = url.Parse(c.baseURL + r.URL) + if err != nil { + return err + } + } + + // Adding Query Param + if len(r.QueryParam) > 0 { + if len(r.QueryParam) > 0 { + if len(strings.TrimSpace(reqURL.RawQuery)) == 0 { + reqURL.RawQuery = r.QueryParam.Encode() + } else { + reqURL.RawQuery = reqURL.RawQuery + "&" + r.QueryParam.Encode() + } + } + } + + r.URL = reqURL.String() + + return nil +} + +// parseRequestHeader merges client and request headers. +// It handles form data by adding content type header. +// +// For Example: +// +// client := &Client{header: http.Header{"Key": {"Value"}}} +// req := &Request{Header: http.Header{"AnotherKey": {"AnotherValue"}}, FormData: map[string][]string{}} +// err := parseRequestHeader(client, req) +// +// Note: Always returns nil error. +func parseRequestHeader(c *Client, r *Request) error { + hdr := make(http.Header) + if c.header != nil { + for k := range c.header { + hdr[k] = append(hdr[k], c.header[k]...) + } + } + + for k := range r.Header { + hdr.Del(k) + hdr[k] = append(hdr[k], r.Header[k]...) + } + + if len(r.FormData) != 0 { + hdr.Add(hdrContentTypeKey, formContentType) + } + + r.Header = hdr + + return nil +} + +// isPayloadSupported checks if the given HTTP method supports a request body. +// It returns true if the method is not HEAD, OPTIONS, GET, or DELETE. +// +// For Example: +// +// isPayloadSupported("POST") // returns true +// isPayloadSupported("GET") // returns false +func isPayloadSupported(m string) bool { + return !(m == consts.MethodHead || m == consts.MethodOptions || m == consts.MethodGet || m == consts.MethodDelete) +} + +// isStringEmpty checks if a given string is empty. +// It trims spaces from both ends and checks if the length is zero. +// +// For Example: +// +// isStringEmpty(" ") // returns true +// isStringEmpty("hello") // returns false +func isStringEmpty(str string) bool { + return len(strings.TrimSpace(str)) == 0 +} + +// IsJSONType method is to check JSON content type or not +func isJSONType(ct string) bool { + return jsonCheck.MatchString(ct) +} + +// IsXMLType method is to check XML content type or not +func isXMLType(ct string) bool { + return xmlCheck.MatchString(ct) +} + +// detectContentType method is used to figure out "request.Body" content type for request header +func detectContentType(body interface{}) string { + contentType := plainTextType + kind := reflect.Indirect(reflect.ValueOf(body)).Kind() + switch kind { + case reflect.Struct, reflect.Map: + contentType = jsonContentType + case reflect.String: + contentType = plainTextType + default: + if b, ok := body.([]byte); ok { + contentType = http.DetectContentType(b) + } else if kind == reflect.Slice { + contentType = jsonContentType + } + } + + return contentType +} + +// parseRequestBody parses HTTP request body and returns content type, body reader, and error. +// It checks if the request method supports payload, determines content type, and serializes body. +// +// For Example: +// +// req := &Request{Method: "POST", Body: []byte("example")} +// contentType, body, err := parseRequestBody(req) +// +// Note: Handles multipart, form data, and detects content type if not specified. +func parseRequestBody(r *Request) (contentType string, body io.Reader, err error) { + if !isPayloadSupported(r.Method) { + return + } + if r.isMultiPart { + return formDataContentType, nil, nil + } else if len(r.FormData) > 0 { + return formContentType, nil, nil + } + var bodyBytes []byte + contentType = r.Header.Get(hdrContentTypeKey) + if isStringEmpty(contentType) { + contentType = detectContentType(r.Body) + r.AddHeader(hdrContentTypeKey, contentType) + } + + switch bodyValue := r.Body.(type) { + case []byte: + bodyBytes = bodyValue + case string: + bodyBytes = []byte(bodyValue) + default: + contentType = r.Header.Get(hdrContentTypeKey) + kind := reflect.Indirect(reflect.ValueOf(r.Body)).Kind() + var err error + if isJSONType(contentType) && (kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) { + bodyBytes, err = json.Marshal(r.Body) + } else if isXMLType(contentType) && (kind == reflect.Struct) { + bodyBytes, err = xml.Marshal(r.Body) + } + if err != nil { + return "", nil, err + } + } + + return contentType, strings.NewReader(string(bodyBytes)), nil +} + +// createHTTPRequest creates an HTTP request based on the given client and request. +// It sets the content type, body, and headers accordingly. +// +// For Example: +// +// client := &Client{} +// req := &Request{Method: "POST", URL: "https://example.com"} +// err := createHTTPRequest(client, req) +// +// Note: Handles multipart and form data, sets cookies and options. +func createHTTPRequest(c *Client, r *Request) (err error) { + contentType, body, err := parseRequestBody(r) + if err != nil { + return err + } + if !isStringEmpty(contentType) { + r.Header.Set(hdrContentTypeKey, contentType) + } + + r.RawRequest = protocol.NewRequest(r.Method, r.URL, body) + if contentType == formDataContentType && isPayloadSupported(r.Method) { + if r.RawRequest.IsBodyStream() { + r.RawRequest.ResetBody() + } + r.RawRequest.SetMultipartFormData(r.MultipartFormParams) + r.RawRequest.SetFiles(r.File) + } else if contentType == formContentType && isPayloadSupported(r.Method) { + r.RawRequest.SetFormDataFromValues(r.FormData) + } + + for key, values := range r.Header { + for _, val := range values { + r.RawRequest.Header.Add(key, val) + } + } + for _, cookie := range r.Cookie { + r.RawRequest.SetCookie(cookie.Name, cookie.Value) + } + + r.RawRequest.SetOptions(r.RequestOptions...) + + return nil +} + +// parseResponseBody parses HTTP response body based on content type (JSON/XML). +// It handles error responses by encoding error info into JSON. +// +// For Example: +// +// client := &Client{} +// resp := &Response{} +// err := parseResponseBody(client, resp) +// +// Note: Handles only JSON or XML content types. +func parseResponseBody(c *Client, resp *Response) (err error) { + if resp.StatusCode() == http.StatusNoContent { + return + } + resp.bodyByte = resp.Body() + resp.size = resp.RawResponse.Header.ContentLength() + // Handles only JSON or XML content type + ct := resp.Header().Get(hdrContentTypeKey) + isError := resp.IsError() + if isError { + jsonByte, jsonErr := json.Marshal(map[string]interface{}{ + "status_code": resp.RawResponse.StatusCode(), + "body": resp.BodyString(), + }) + if jsonErr != nil { + return jsonErr + } + err = fmt.Errorf(string(jsonByte)) + } else if resp.Request.Result != nil { + if isJSONType(ct) || isXMLType(ct) { + err = unmarshalContent(ct, resp.Body(), resp.Request.Result) + return + } + } + return +} + +// unmarshalContent content into object from JSON or XML +func unmarshalContent(ct string, b []byte, d interface{}) (err error) { + if isJSONType(ct) { + err = json.Unmarshal(b, d) + } else if isXMLType(ct) { + err = xml.Unmarshal(b, d) + } + + return +} diff --git a/request.go b/request.go new file mode 100644 index 0000000..4f8913c --- /dev/null +++ b/request.go @@ -0,0 +1,732 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package easy_http + +import ( + "context" + "fmt" + "net/http" + "net/url" + "reflect" + "strings" + "time" + + "github.com/cloudwego/hertz/pkg/common/config" + "github.com/cloudwego/hertz/pkg/protocol" + "github.com/cloudwego/hertz/pkg/protocol/consts" + "github.com/li-jin-gou/http2curl" +) + +type Request struct { + client *Client + URL string + Method string + QueryParam url.Values + FormData url.Values + Header http.Header + Cookie []*http.Cookie + Body interface{} + PathParams map[string]string + MultipartFormParams map[string]string + File map[string]string + RawRequest *protocol.Request + Ctx context.Context + RequestOptions []config.RequestOption + Result interface{} + Error error + isMultiPart bool + hasCreate bool +} + +// SetQueryParam method sets single parameter and its value in the current request. +// It will be formed as query string for the request. +// +// For Example: `search=kitchen%20papers&size=large` in the URL after `?` mark. +// +// client.R(). +// SetQueryParam("search", "kitchen papers"). +// SetQueryParam("size", "large") +// +// Note: it will overwrite the same query key. +func (r *Request) SetQueryParam(param, value string) *Request { + r.QueryParam.Set(param, value) + return r +} + +// SetQueryParams sets multiple query parameters from a map. +// It iterates over the map and sets each key-value pair. +// +// For Example: +// +// client.R(). +// SetQueryParams(map[string]string{ +// "search": "kitchen papers", +// "size": "large", +// }) +// +// Note: it will overwrite existing parameters with the same keys. +func (r *Request) SetQueryParams(params map[string]string) *Request { + for p, v := range params { + r.SetQueryParam(p, v) + } + return r +} + +// SetQueryParamsFromValues sets query parameters from url.Values. +// It iterates over each key-value pair and adds them to the request. +// +// For Example: +// +// params := url.Values{} +// params.Add("search", "kitchen papers") +// params.Add("size", "large") +// client.R(). +// SetQueryParamsFromValues(params) +// +// Note: it avoids slice case by using 'add'. +func (r *Request) SetQueryParamsFromValues(params url.Values) *Request { + for p, v := range params { + for _, pv := range v { + // use 'add' to avoid slice case + r.QueryParam.Add(p, pv) + } + } + return r +} + +// SetQueryString sets the query string for the request. +// It parses the input query string and sets the query parameters. +// +// For Example: +// +// r := &Request{} +// r.SetQueryString("param1=value1¶m2=value2") +// +// Note: If parsing fails, it sets the error in the Request. +func (r *Request) SetQueryString(query string) *Request { + q, err := url.ParseQuery(strings.TrimSpace(query)) + if err != nil { + r.Error = err + return r + } + r.SetQueryParamsFromValues(q) + return r +} + +// AddQueryParam method adds a query parameter to the current request. +// It accepts two strings: params (key) and value (value). +// +// For Example: +// +// client.R(). +// AddQueryParam("key1", "value1"). +// AddQueryParam("key2", "value2") +// +// Returns the modified Request object for chaining. +func (r *Request) AddQueryParam(params, value string) *Request { + r.QueryParam.Add(params, value) + return r +} + +// AddQueryParams adds multiple query parameters to the request. +// It accepts a map of string keys and values. +// +// For Example: +// +// client.R(). +// AddQueryParams(map[string]string{"search": "kitchen papers", "size": "large"}) +// +// Note: it will append to existing query parameters. +func (r *Request) AddQueryParams(params map[string]string) *Request { + for k, v := range params { + r.AddQueryParam(k, v) + } + return r +} + +// AddQueryParamsFromValues method adds multiple query parameters from url.Values to the request. +// It iterates over the provided url.Values and adds each key-value pair to the request's query parameters. +// +// For Example: +// +// params := url.Values{} +// params.Add("key1", "value1") +// params.Add("key2", "value2") +// client.R(). +// AddQueryParamsFromValues(params) +// +// Note: This method supports chaining. +func (r *Request) AddQueryParamsFromValues(params url.Values) *Request { + for p, v := range params { + for _, pv := range v { + r.QueryParam.Add(p, pv) + } + } + return r +} + +// SetPathParam sets a path parameter in the Request object. +// It takes two string arguments: param and value. +// +// Example: +// +// r := &Request{} +// r.SetPathParam("id", "123") +// +// This method modifies the Request object and returns it. +func (r *Request) SetPathParam(param, value string) *Request { + r.PathParams[param] = value + return r +} + +// SetPathParams sets multiple path parameters in the request. +// It iterates over the provided map and sets each parameter. +// +// For Example: +// +// r := &Request{} +// r.SetPathParams(map[string]string{ +// "param1": "value1", +// "param2": "value2", +// }) +// +// Note: This will overwrite existing parameters with the same name. +func (r *Request) SetPathParams(params map[string]string) *Request { + for p, v := range params { + r.SetPathParam(p, v) + } + return r +} + +// SetHeader method sets a header field and its value in the current request. +// It supports chaining for easier method calls. +// +// For Example: +// +// client.R(). +// SetHeader("Content-Type", "application/json"). +// SetHeader("Authorization", "Bearer token") +// +// Note: it will overwrite the same header key. +func (r *Request) SetHeader(header, value string) *Request { + r.Header.Set(header, value) + return r +} + +// SetHeaders method sets multiple HTTP headers in the current request. +// It iterates over the provided map and sets each header individually. +// +// For Example: +// +// client.R(). +// SetHeaders(map[string]string{ +// "Content-Type": "application/json", +// "Authorization": "Bearer token", +// }) +// +// Note: it will overwrite existing headers with the same key. +func (r *Request) SetHeaders(headers map[string]string) *Request { + for h, v := range headers { + r.SetHeader(h, v) + } + return r +} + +// SetHTTPHeader sets HTTP request headers. +// It accepts http.Header and returns the modified Request. +// +// For Example: +// +// headers := http.Header{} +// headers.Add("Content-Type", "application/json") +// client.R(). +// SetHTTPHeader(headers) +func (r *Request) SetHTTPHeader(header http.Header) *Request { + r.Header = header + return r +} + +// AddHeader adds a new HTTP header to the Request. +// It accepts header name and value as strings. +// +// For Example: +// +// r := &Request{} +// r.AddHeader("Content-Type", "application/json") +// +// Returns the updated Request object for chaining. +func (r *Request) AddHeader(header, value string) *Request { + r.Header.Add(header, value) + return r +} + +// AddHeaders method adds multiple HTTP headers to the request. +// It iterates over the provided map and adds each header using AddHeader. +// +// For Example: +// +// headers := map[string]string{ +// "Content-Type": "application/json", +// "Authorization": "Bearer token", +// } +// client.R(). +// AddHeaders(headers) +// +// Note: Each header will be added individually. +func (r *Request) AddHeaders(headers map[string]string) *Request { + for k, v := range headers { + r.AddHeader(k, v) + } + return r +} + +// AddHTTPHeader method adds HTTP headers to the request. +// It accepts a http.Header and appends each key-value pair to the request header. +// +// For Example: +// +// headers := http.Header{} +// headers.Add("Content-Type", "application/json") +// client.R(). +// AddHTTPHeader(headers) +// +// Note: it will append to existing headers. +func (r *Request) AddHTTPHeader(header http.Header) *Request { + for key, value := range header { + for _, v := range value { + r.Header.Add(key, v) + } + } + return r +} + +// SetContentType sets the Content-Type header for the HTTP request. +// It accepts a string parameter ct representing the Content-Type value. +// +// For Example: +// +// client.R(). +// SetContentType("application/json") +// +// Returns the request object itself for chaining. +func (r *Request) SetContentType(ct string) *Request { + r.Header.Add(consts.HeaderContentType, ct) + return r +} + +// SetContentTypeJSON sets the Content-Type header to application/json. +// It returns the Request instance for chaining. +// +// Example: +// +// client.R(). +// SetContentTypeJSON() +func (r *Request) SetContentTypeJSON() *Request { + r.Header.Add(consts.HeaderContentType, consts.MIMEApplicationJSON) + return r +} + +// SetContentTypeFormData sets the HTTP request content type to multipart/form-data. +// It adds the specific content type header to the request header. +// +// For Example: +// +// req := client.R(). +// SetContentTypeFormData() +// +// Returns the modified request object for chaining. +func (r *Request) SetContentTypeFormData() *Request { + r.Header.Add(consts.HeaderContentType, consts.MIMEMultipartPOSTForm) + return r +} + +// SetContentTypeUrlEncode sets the Content-Type header to application/x-www-form-urlencoded. +// This is useful for encoding form data as URL parameters. +// +// Example: +// +// r := client.R(). +// SetContentTypeUrlEncode() +func (r *Request) SetContentTypeUrlEncode() *Request { + r.Header.Add(consts.HeaderContentType, consts.MIMEApplicationHTMLForm) + return r +} + +// todo 确认 cookie 的具体实现 +func (r *Request) SetCookie(hc *http.Cookie) *Request { + r.Cookie = append(r.Cookie, hc) + return r +} + +// SetCookies adds a slice of HTTP cookies to the Request object. +// It appends the given cookies to the existing list. +// +// For Example: +// +// cookies := []*http.Cookie{ +// {Name: "cookie1", Value: "value1"}, +// {Name: "cookie2", Value: "value2"}, +// } +// client.R().SetCookies(cookies) +// +// Note: This method modifies the Request object in place. +func (r *Request) SetCookies(rs []*http.Cookie) *Request { + r.Cookie = append(r.Cookie, rs...) + return r +} + +// SetBody sets the body of the HTTP request. +// It accepts an interface{} type and returns the request object. +// +// For Example: +// +// req := &Request{} +// req.SetBody(map[string]interface{}{"key": "value"}) +func (r *Request) SetBody(body interface{}) *Request { + r.Body = body + return r +} + +// SetFormData sets form data in the request. +// It accepts a map of form field names and values. +// +// For Example: +// +// req := client.R(). +// SetFormData(map[string]string{ +// "name": "John", +// "age": "30", +// }) +// +// Note: it will overwrite existing form data fields. +func (r *Request) SetFormData(data map[string]string) *Request { + for k, v := range data { + r.FormData.Set(k, v) + } + return r +} + +// SetFormDataFromValues adds url.Values to the request's FormData. +// It iterates over the provided url.Values and adds each key-value pair. +// +// For Example: +// +// data := url.Values{} +// data.Add("key1", "value1") +// data.Add("key2", "value2") +// client.R(). +// SetFormDataFromValues(data) +// +// Note: it will append to existing FormData. +func (r *Request) SetFormDataFromValues(data url.Values) *Request { + for key, value := range data { + for _, v := range value { + r.FormData.Add(key, v) + } + } + return r +} + +// SetMultipartFormData sets multipart form data for the request. +// It marks the request as multipart and sets the form parameters. +// +// For Example: +// +// data := map[string]string{ +// "key1": "value1", +// "key2": "value2", +// } +// client.R(). +// SetMultipartFormData(data) +// +// Note: This method supports chaining. +func (r *Request) SetMultipartFormData(data map[string]string) *Request { + r.isMultiPart = true + r.MultipartFormParams = data + return r +} + +// SetFile sets a file in the HTTP request. +// It marks the request as multipart and stores the file info. +// +// For Example: +// +// r := &Request{} +// r.SetFile("example.txt", "/path/to/example.txt") +// +// Note: This function does not check if the file exists. +func (r *Request) SetFile(filename, filepath string) *Request { + r.isMultiPart = true + r.File[filename] = filepath + return r +} + +// SetFiles sets HTTP request files and marks it as multipart form. +// It accepts a map where keys are filenames and values are file paths. +// +// For Example: +// +// request.SetFiles(map[string]string{ +// "file1": "/path/to/file1", +// "file2": "/path/to/file2", +// }) +// +// Returns the updated request object. +func (r *Request) SetFiles(files map[string]string) *Request { + r.isMultiPart = true + r.File = files + return r +} + +// SetResult sets the result value of the Request object. +// It accepts an interface{} and sets the Result field based on whether the input is a pointer. +// +// For Example: +// +// req := &Request{} +// req.SetResult(&MyStruct{}). +// SetResult("stringValue") +// +// Note: If the input is not a pointer, a new pointer instance is created. +func (r *Request) SetResult(res interface{}) *Request { + if res != nil { + vv := reflect.ValueOf(res) + if vv.Kind() == reflect.Ptr { + r.Result = res + } else { + r.Result = reflect.New(vv.Type()).Interface() + } + } + return r +} + +// withContext method attaches a context to the Request instance. +// It accepts a context.Context and assigns it to the Ctx field. +// +// For Example: +// +// req := &Request{} +// req.withContext(context.Background()) +// +// Note: This method does not handle context cancellation. +func (r *Request) withContext(ctx context.Context) *Request { + r.Ctx = ctx + return r +} + +// WithDC returns the current request object pointer. +// It does not modify the request. +// +// For Example: +// +// req := &Request{} +// req = req.WithDC() +func (r *Request) WithDC() *Request { + return r +} + +// WithCluster method returns the current Request instance. +// It does not modify the receiver. +// +// For Example: +// +// req := &Request{} +// req = req.WithCluster() +func (r *Request) WithCluster() *Request { + return r +} + +// WithEnv returns the current Request instance. +// It does not modify the Request object. +// +// For Example: +// +// req := &Request{} +// req.WithEnv() +func (r *Request) WithEnv() *Request { + return r +} + +// WithRequestTimeout sets the request timeout duration. +// It modifies the request's timeout settings. +// +// For Example: +// +// r := &Request{} +// r.WithRequestTimeout(5 * time.Second) +// +// Note: This method modifies the request instance. +func (r *Request) WithRequestTimeout(t time.Duration) *Request { + r.RawRequest.SetOptions(config.WithRequestTimeout(t)) + return r +} + +// Get method executes an HTTP GET request in the given context. +// It associates the context with the request and then performs the GET request using the specified URL. +// +// For Example: +// +// resp, err := client.R(). +// Get(ctx, "https://example.com/api/resource") +// +// Returns the response and any error encountered. +func (r *Request) Get(ctx context.Context, url string) (*Response, error) { + r.withContext(ctx) + return r.Execute(consts.MethodGet, url) +} + +// Head method sends an HTTP HEAD request to the specified URL. +// It attaches the context to the request and executes it. +// +// For Example: +// +// resp, err := client.R(). +// Head(ctx, "https://example.com") +// +// Note: Returns response and possible error. +func (r *Request) Head(ctx context.Context, url string) (*Response, error) { + r.withContext(ctx) + return r.Execute(consts.MethodHead, url) +} + +// Post method executes HTTP POST request with given context and URL. +// It associates context with request, then performs POST action. +// +// For Example: +// +// resp, err := client.R(). +// Post(ctx, "https://example.com/api") +// +// Note: Returns response and possible error. +func (r *Request) Post(ctx context.Context, url string) (*Response, error) { + r.withContext(ctx) + return r.Execute(consts.MethodPost, url) +} + +// Put method sends a PUT request to the specified URL with the given context. +// It attaches the context to the request and executes it with the PUT method. +// +// For Example: +// +// resp, err := client.R(). +// Put(ctx, "https://example.com/resource") +// +// Returns the response and any error encountered. +func (r *Request) Put(ctx context.Context, url string) (*Response, error) { + r.withContext(ctx) + return r.Execute(consts.MethodPut, url) +} + +// Delete method executes an HTTP DELETE request in the given context. +// It attaches the context to the request and then performs the DELETE operation. +// +// For Example: +// +// resp, err := client.R(). +// Delete(ctx, "https://example.com/resource") +// +// Note: Returns the response and any error encountered. +func (r *Request) Delete(ctx context.Context, url string) (*Response, error) { + r.withContext(ctx) + return r.Execute(consts.MethodDelete, url) +} + +// Options method executes an HTTP OPTIONS request with the given context and URL. +// It attaches the context to the request and then calls Execute to perform the request. +// +// For Example: +// +// resp, err := client.R(). +// Options(ctx, "https://example.com") +// +// Note: Returns the response and any error encountered. +func (r *Request) Options(ctx context.Context, url string) (*Response, error) { + r.withContext(ctx) + return r.Execute(consts.MethodOptions, url) +} + +// Patch method performs a PATCH request to the specified URL with the given context. +// It associates the context with the request and then executes the PATCH request. +// +// For Example: +// +// resp, err := client.R(). +// Patch(ctx, "https://example.com/api") +// +// Note: Ensure the context is properly managed to avoid goroutine leaks. +func (r *Request) Patch(ctx context.Context, url string) (*Response, error) { + r.withContext(ctx) + return r.Execute(consts.MethodPatch, url) +} + +// Send sends an HTTP request with the given context. +// It associates the context with the request and executes it. +// +// For Example: +// +// resp, err := client.R(). +// Send(ctx) +// +// Returns the response or an error if the request fails. +func (r *Request) Send(ctx context.Context) (*Response, error) { + r.withContext(ctx) + return r.Execute(r.Method, r.URL) +} + +// ToCurl converts an HTTP request to a curl command string. +// It checks if the request has been created and returns an error if not. +// +// For Example: +// +// req, _ := http.NewRequest("GET", "http://example.com", nil) +// curlCmd, err := req.ToCurl() +// if err != nil { +// log.Fatalf("Error: %s", err) +// } +// fmt.Println(curlCmd) +// +// Note: Ensure the request is created before calling this method. +func (r *Request) ToCurl() (string, error) { + if !r.hasCreate { + return "", fmt.Errorf("request has not been create") + } + c, err := http2curl.GetCurlCommandHertz(r.RawRequest) + if err != nil { + return "", err + } + return c.String(), nil +} + +// Execute method executes an HTTP request with the given method and URL. +// It sets the request method and URL, checks for any existing errors, +// and then calls the client's execute method to perform the request. +// +// For Example: +// +// resp, err := client.R(). +// Execute("GET", "https://example.com/api") +// +// Returns the response and any error encountered. +func (r *Request) Execute(method, url string) (*Response, error) { + r.Method = method + r.URL = url + if r.Error != nil { + return nil, r.Error + } + + return r.client.execute(r) +} diff --git a/response.go b/response.go new file mode 100644 index 0000000..f9532c1 --- /dev/null +++ b/response.go @@ -0,0 +1,221 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package easy_http + +import ( + "bytes" + "io" + "net/http" + + "github.com/cloudwego/hertz/pkg/protocol" +) + +type Response struct { + Request *Request + RawResponse *protocol.Response + + bodyByte []byte + size int +} + +// Body method returns the response body content from the Response object. +// If the RawResponse is nil, it returns an empty byte slice. +// +// For Example: +// +// resp := client.Get("/endpoint") +// body := resp.Body() +// +// Note: Ensure RawResponse is not nil before calling this method. +func (r *Response) Body() []byte { + if r.RawResponse == nil { + return []byte{} + } + return r.RawResponse.Body() +} + +// BodyString returns the response body as a string. +// If RawResponse is nil, it returns an empty string. +// +// For Example: +// +// resp := &Response{RawResponse: someHTTPResponse} +// fmt.Println(resp.BodyString()) +func (r *Response) BodyString() string { + if r.RawResponse == nil { + return "" + } + return string(r.RawResponse.Body()) +} + +// StatusCode returns the HTTP response status code. +// If RawResponse is nil, it returns 0. +// +// For Example: +// +// resp := client.R().Get("/endpoint") +// code := resp.StatusCode() +// +// Note: Ensure RawResponse is not nil before calling. +func (r *Response) StatusCode() int { + if r.RawResponse == nil { + return 0 + } + return r.RawResponse.StatusCode() +} + +// Result method returns the result of the request from the Response object. +// The result type is an interface{}, which can be any type depending on the request. +// +// For Example: +// +// resp := client.R().Get("/endpoint") +// result := resp.Result() +// +// Note: Ensure to type assert the result to the expected type. +func (r *Response) Result() interface{} { + return r.Request.Result +} + +// GetRequest returns the Request instance associated with the Response. +// +// For Example: +// +// resp := &Response{} +// req := resp.GetRequest() +func (r *Response) GetRequest() *Request { + return r.Request +} + +// GetRawResponse returns the raw protocol response from the Response struct. +// +// For Example: +// +// resp := &Response{RawResponse: &protocol.Response{...}} +// rawResp := resp.GetRawResponse() +func (r *Response) GetRawResponse() *protocol.Response { + return r.RawResponse +} + +// Error method retrieves the error from the associated Request object. +// It returns an error indicating any issues encountered during the Request. +// +// For Example: +// +// err := response.Error() +// if err != nil { +// fmt.Println("Error:", err) +// } +func (r *Response) Error() error { + return r.Request.Error +} + +// Header method extracts HTTP headers from the Response object. +// If RawResponse is nil, it returns an empty http.Header. +// +// For Example: +// +// resp := client.Get("/example") +// headers := resp.Header() +// +// Note: This method does not modify the Response object. +func (r *Response) Header() http.Header { + if r.RawResponse == nil { + return http.Header{} + } + header := make(http.Header) + r.RawResponse.Header.VisitAll(func(key, value []byte) { + header.Add(string(key), string(value)) + }) + return header +} + +// Cookies method extracts all cookies from the HTTP response. +// It returns a slice of http.Cookie. +// +// For Example: +// +// cookies := response.Cookies() +// for _, cookie := range cookies { +// fmt.Println(cookie.Name, cookie.Value) +// } +// +// Note: Returns an empty slice if RawResponse is nil. +func (r *Response) Cookies() []*http.Cookie { + if r.RawResponse == nil { + return make([]*http.Cookie, 0) + } + var cookies []*http.Cookie + r.RawResponse.Header.VisitAllCookie(func(key, value []byte) { + cookies = append(cookies, &http.Cookie{ + Name: string(key), + Value: string(value), + }) + }) + + return cookies +} + +// ToRawHTTPResponse converts Response object to raw HTTP response string. +// It sets StatusCode, Header, and Body from Response object. +// +// For Example: +// +// resp := &Response{} +// rawHTTP := resp.ToRawHTTPResponse() +// +// Note: Ensure Response object is properly initialized. +func (r *Response) ToRawHTTPResponse() string { + resp := &http.Response{ + StatusCode: r.StatusCode(), + Header: r.Header(), + Body: io.NopCloser(bytes.NewReader(r.Body())), + } + for _, cookie := range r.Cookies() { + resp.Header.Add("Set-Cookie", cookie.String()) + } + var buffer bytes.Buffer + resp.Write(&buffer) + + return buffer.String() +} + +// IsSuccess checks if the HTTP response is successful. +// It returns true if the status code is between 200 and 299. +// +// For Example: +// +// resp := client.R().Get("/endpoint") +// if resp.IsSuccess() { +// fmt.Println("Request was successful") +// } +func (r *Response) IsSuccess() bool { + return r.StatusCode() > 199 && r.StatusCode() < 300 +} + +// IsError checks if the HTTP response indicates an error. +// It returns true if the status code is greater than 399. +// +// Example: +// +// resp := client.Get("/endpoint") +// if resp.IsError() { +// fmt.Println("Error in response") +// } +func (r *Response) IsError() bool { + return r.StatusCode() > 399 +}