github.com/IBM-Cloud/bluemix-go@v0.0.0-20240423071914-9e96525baef4/rest/client.go (about) 1 // Package rest provides a simple REST client for creating and sending 2 // API requests. 3 4 // Examples: 5 // Creating request 6 // // GET request 7 // GetRequest("http://www.example.com"). 8 // Set("Accept", "application/json"). 9 // Query("foo1", "bar1"). 10 // Query("foo2", "bar2") 11 // 12 // // JSON body 13 // foo = Foo{Bar: "val"} 14 // PostRequest("http://www.example.com"). 15 // Body(foo) 16 17 // // String body 18 // PostRequest("http://www.example.com"). 19 // Body("{\"bar\": \"val\"}") 20 21 // // Stream body 22 // PostRequest("http://www.example.com"). 23 // Body(strings.NewReader("abcde")) 24 25 // // Multipart POST request 26 // var f *os.File 27 // PostRequest("http://www.example.com"). 28 // Field("foo", "bar"). 29 // File("file1", File{Name: f.Name(), Content: f}). 30 // File("file2", File{Name: "1.txt", Content: []byte("abcde"), Type: "text/plain") 31 32 // // Build to an HTTP request 33 // GetRequest("http://www.example.com").Build() 34 35 // Sending request: 36 // client := NewClient() 37 // var foo = struct { 38 // Bar string 39 // }{} 40 // var apiErr = struct { 41 // Message string 42 // }{} 43 // resp, err := client.Do(request, &foo, &apiErr) 44 package rest 45 46 import ( 47 "encoding/json" 48 "fmt" 49 "io" 50 "io/ioutil" 51 "net/http" 52 "reflect" 53 54 "github.com/IBM-Cloud/bluemix-go/bmxerror" 55 ) 56 57 const ( 58 //ErrCodeEmptyResponse ... 59 ErrCodeEmptyResponse = "EmptyResponseBody" 60 ) 61 62 //ErrEmptyResponseBody ... 63 var ErrEmptyResponseBody = bmxerror.New(ErrCodeEmptyResponse, "empty response body") 64 65 // Client is a REST client. It's recommend that a client be created with the 66 // NewClient() method. 67 type Client struct { 68 // The HTTP client to be used. Default is HTTP's defaultClient. 69 HTTPClient *http.Client 70 // Defaualt header for all outgoing HTTP requests. 71 DefaultHeader http.Header 72 } 73 74 // NewClient creates a new REST client. 75 func NewClient() *Client { 76 return &Client{ 77 HTTPClient: http.DefaultClient, 78 } 79 } 80 81 // Do sends an request and returns an HTTP response. The resp.Body will be 82 // consumed and closed in the method. 83 // 84 // For 2XX response, it will be JSON decoded into the value pointed to by 85 // respv. 86 // 87 // For non-2XX response, an attempt will be made to unmarshal the response 88 // into the value pointed to by errV. If unmarshal failed, an ErrorResponse 89 // error with status code and response text is returned. 90 func (c *Client) Do(r *Request, respV interface{}, errV interface{}) (*http.Response, error) { 91 req, err := c.makeRequest(r) 92 if err != nil { 93 return nil, err 94 } 95 96 client := c.HTTPClient 97 if client == nil { 98 client = http.DefaultClient 99 } 100 101 resp, err := client.Do(req) 102 if err != nil { 103 return resp, err 104 } 105 defer resp.Body.Close() 106 107 if resp.StatusCode < 200 || resp.StatusCode > 299 { 108 raw, err := ioutil.ReadAll(resp.Body) 109 if err != nil { 110 return resp, fmt.Errorf("Error reading response: %v", err) 111 } 112 113 if len(raw) > 0 && errV != nil { 114 if json.Unmarshal(raw, errV) == nil { 115 return resp, nil 116 } 117 } 118 119 return resp, bmxerror.NewRequestFailure("ServerErrorResponse", string(raw), resp.StatusCode) 120 } 121 122 if respV != nil { 123 // Callback function with execpted JSON type 124 if funcType := reflect.TypeOf(respV); funcType.Kind() == reflect.Func { 125 if funcType.NumIn() != 1 || funcType.NumOut() != 1 { 126 err = fmt.Errorf("Callback funcion not expected signature: func(interface{}) bool") 127 } 128 paramType := funcType.In(0) 129 dc := json.NewDecoder(resp.Body) 130 dc.UseNumber() 131 for { 132 typedInterface := reflect.New(paramType).Interface() 133 if err = dc.Decode(typedInterface); err == io.EOF { 134 err = nil 135 break 136 } else if err != nil { 137 break 138 } 139 resv := reflect.ValueOf(respV).Call([]reflect.Value{reflect.ValueOf(typedInterface).Elem()})[0] 140 if !resv.Bool() { 141 break 142 } 143 } 144 } else { 145 switch respV.(type) { 146 case io.Writer: 147 _, err = io.Copy(respV.(io.Writer), resp.Body) 148 default: 149 dc := json.NewDecoder(resp.Body) 150 dc.UseNumber() 151 err = dc.Decode(respV) 152 if err == io.EOF { 153 err = ErrEmptyResponseBody 154 } 155 } 156 } 157 } 158 159 return resp, err 160 } 161 162 func (c *Client) makeRequest(r *Request) (*http.Request, error) { 163 req, err := r.Build() 164 if err != nil { 165 return nil, err 166 } 167 168 c.applyDefaultHeader(req) 169 170 if req.Header.Get("Accept") == "" { 171 req.Header.Set("Accept", "application/json") 172 } 173 if req.Header.Get("Content-Type") == "" { 174 req.Header.Set("Content-Type", "application/json") 175 } 176 if req.Header.Get("Accept-Language") == "" { 177 req.Header.Set("Accept-Language", "en") 178 } 179 180 return req, nil 181 } 182 183 func (c *Client) applyDefaultHeader(req *http.Request) { 184 for k, vs := range c.DefaultHeader { 185 if req.Header.Get(k) != "" { 186 continue 187 } 188 for _, v := range vs { 189 req.Header.Add(k, v) 190 } 191 } 192 }