github.com/schmorrison/Zoho@v1.1.4/http.go (about) 1 package zoho 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "mime/multipart" 10 "net/http" 11 "net/url" 12 "os" 13 "path/filepath" 14 "reflect" 15 "strings" 16 17 "github.com/schmorrison/go-querystring/query" 18 ) 19 20 // Endpoint defines the data required to interact with most Zoho REST api endpoints 21 type Endpoint struct { 22 Method HTTPMethod 23 URL string 24 Name string 25 ResponseData interface{} 26 RequestBody interface{} 27 URLParameters map[string]Parameter 28 Headers map[string]string 29 BodyFormat BodyFormat 30 Attachment string 31 } 32 33 // Parameter is used to provide URL Parameters to zoho endpoints 34 type Parameter string 35 type BodyFormat string 36 37 const ( 38 JSON = "" 39 JSON_STRING = "jsonString" 40 FILE = "file" 41 URL = "url" // Added new BodyFormat option 42 ) 43 44 // HTTPRequest is the function which actually performs the request to a Zoho endpoint as specified by the provided endpoint 45 func (z *Zoho) HTTPRequest(endpoint *Endpoint) (err error) { 46 if reflect.TypeOf(endpoint.ResponseData).Kind() != reflect.Ptr { 47 return fmt.Errorf("Failed, you must pass a pointer in the ResponseData field of endpoint") 48 } 49 50 // Load and renew access token if expired 51 err = z.CheckForSavedTokens() 52 if err == ErrTokenExpired { 53 err := z.RefreshTokenRequest() 54 if err != nil { 55 return fmt.Errorf("Failed to refresh the access token: %s: %s", endpoint.Name, err) 56 } 57 } 58 59 // Retrieve URL parameters 60 endpointURL := endpoint.URL 61 q := url.Values{} 62 for k, v := range endpoint.URLParameters { 63 if v != "" { 64 q.Set(k, string(v)) 65 } 66 } 67 68 var ( 69 req *http.Request 70 reqBody io.Reader 71 contentType string 72 ) 73 74 // Has a body, likely a CRUD operation (still possibly JSONString) 75 if endpoint.BodyFormat == JSON || endpoint.BodyFormat == JSON_STRING { 76 if endpoint.RequestBody != nil { 77 // JSON Marshal the body 78 marshalledBody, err := json.Marshal(endpoint.RequestBody) 79 if err != nil { 80 return fmt.Errorf("Failed to create json from request body: %s", err) 81 } 82 83 reqBody = bytes.NewReader(marshalledBody) 84 } 85 contentType = "application/json; charset=UTF-8" 86 } 87 88 if endpoint.BodyFormat == JSON_STRING || endpoint.BodyFormat == FILE { 89 // Create a multipart form 90 var b bytes.Buffer 91 w := multipart.NewWriter(&b) 92 93 switch endpoint.BodyFormat { 94 case JSON_STRING: 95 // Use the form to create the proper field 96 fw, err := w.CreateFormField("JSONString") 97 if err != nil { 98 return err 99 } 100 // Copy the request body JSON into the field 101 if _, err = io.Copy(fw, reqBody); err != nil { 102 return err 103 } 104 105 // Close the multipart writer to set the terminating boundary 106 err = w.Close() 107 if err != nil { 108 return err 109 } 110 111 case FILE: 112 // Retreive the file contents 113 fileReader, err := os.Open(endpoint.Attachment) 114 if err != nil { 115 return err 116 } 117 defer fileReader.Close() 118 // Create the correct form field 119 part, err := w.CreateFormFile("attachment", filepath.Base(endpoint.Attachment)) 120 if err != nil { 121 return err 122 } 123 // copy the file contents to the form 124 if _, err = io.Copy(part, fileReader); err != nil { 125 return err 126 } 127 128 err = w.Close() 129 if err != nil { 130 return err 131 } 132 } 133 134 reqBody = &b 135 contentType = w.FormDataContentType() 136 } 137 138 // New BodyFormat encoding option 139 if endpoint.BodyFormat == URL { 140 body, err := query.Values(endpoint.RequestBody) // send struct into the newly imported package 141 if err != nil { 142 return err 143 } 144 145 reqBody = strings.NewReader(body.Encode()) // write to body 146 contentType = "application/x-www-form-urlencoded; charset=UTF-8" 147 } 148 149 req, err = http.NewRequest(string(endpoint.Method), fmt.Sprintf("%s?%s", endpointURL, q.Encode()), reqBody) 150 if err != nil { 151 return fmt.Errorf("Failed to create a request for %s: %s", endpoint.Name, err) 152 } 153 154 req.Header.Set("Content-Type", contentType) 155 156 // Add global authorization header 157 req.Header.Add("Authorization", "Zoho-oauthtoken "+z.oauth.token.AccessToken) 158 159 // Add specific endpoint headers 160 for k, v := range endpoint.Headers { 161 req.Header.Add(k, v) 162 } 163 164 resp, err := z.client.Do(req) 165 if err != nil { 166 return fmt.Errorf("Failed to perform request for %s: %s", endpoint.Name, err) 167 } 168 169 defer resp.Body.Close() 170 171 body, err := ioutil.ReadAll(resp.Body) 172 if err != nil { 173 return fmt.Errorf("Failed to read body of response for %s: got status %s: %s", endpoint.Name, resolveStatus(resp), err) 174 } 175 176 dataType := reflect.TypeOf(endpoint.ResponseData).Elem() 177 data := reflect.New(dataType).Interface() 178 179 if len(body) > 0 { // Avoid failed to unmarshal if there is no result 180 err = json.Unmarshal(body, data) 181 if err != nil { 182 return fmt.Errorf("Failed to unmarshal data from response for %s: got status %s: %s", endpoint.Name, resolveStatus(resp), err) 183 } 184 185 // Search for hidden errors (appears on success response) 186 if bytes.Contains(body, []byte(`"status":"error"`)) { 187 return fmt.Errorf("%s", string(body)) 188 } 189 } 190 191 endpoint.ResponseData = data 192 193 return nil 194 } 195 196 // HTTPStatusCode is a type for resolving the returned HTTP Status Code Content 197 type HTTPStatusCode int 198 199 // HTTPStatusCodes is a map of possible HTTP Status Code and Messages 200 var HTTPStatusCodes = map[HTTPStatusCode]string{ 201 200: "The API request is successful.", 202 201: "Request fulfilled for single record insertion.", 203 202: "Request fulfilled for multiple records insertion.", 204 204: "There is no content available for the request.", 205 304: "The requested page has not been modified. In case \"If-Modified-Since\" header is used for GET APIs", 206 400: "The request or the authentication considered is invalid.", 207 401: "Invalid API key provided.", 208 403: "No permission to do the operation.", 209 404: "Invalid request.", 210 405: "The specified method is not allowed.", 211 413: "The server did not accept the request while uploading a file, since the limited file size has exceeded.", 212 415: "The server did not accept the request while uploading a file, since the media/ file type is not supported.", 213 429: "Number of API requests per minute/day has exceeded the limit.", 214 500: "Generic error that is encountered due to an unexpected server error.", 215 } 216 217 func resolveStatus(r *http.Response) string { 218 if v, ok := HTTPStatusCodes[HTTPStatusCode(r.StatusCode)]; ok { 219 return v 220 } 221 return "" 222 } 223 224 // HTTPHeader is a type for defining possible HTTPHeaders that zoho request could return 225 type HTTPHeader string 226 227 const ( 228 rateLimit HTTPHeader = "X-RATELIMIT-LIMIT" 229 rateLimitRemaining HTTPHeader = "X-RATELIMIT-REMAINING" 230 rateLimitReset HTTPHeader = "X-RATELIMIT-RESET" 231 ) 232 233 func checkHeaders(r http.Response, header HTTPHeader) string { 234 value := r.Header.Get(string(header)) 235 236 if value != "" { 237 return value 238 } 239 return "" 240 } 241 242 // HTTPMethod is a type for defining the possible HTTP request methods that can be used 243 type HTTPMethod string 244 245 const ( 246 // HTTPGet is the GET method for http requests 247 HTTPGet HTTPMethod = "GET" 248 // HTTPPost is the POST method for http requests 249 HTTPPost HTTPMethod = "POST" 250 // HTTPPut is the PUT method for http requests 251 HTTPPut HTTPMethod = "PUT" 252 // HTTPDelete is the DELETE method for http requests 253 HTTPDelete HTTPMethod = "DELETE" 254 )