├── README.md ├── gencurl.go └── gencurl_test.go /README.md: -------------------------------------------------------------------------------- 1 | # gencurl 2 | gencurl generates a curl command based on an http.Request to be used for logging and debugging 3 | 4 | ```go 5 | // create an http request 6 | data := []byte(`{"key":"value"}`) 7 | req, err := http.NewRequest("POST", "http://www.example.com", bytes.NewBuffer(data)) 8 | if err != nil { 9 | // handle err 10 | } 11 | req.Header.Add("X-Custom", "custom data") 12 | 13 | curl := gencurl.FromRequest(req) 14 | 15 | // later, execute the request. On error, you can print curl to replicate and debug an issue 16 | ``` 17 | 18 | The generated curl command for this example would be: 19 | `curl -v -X POST --header 'X-Custom: custom data' http://www.example.com -d '{"key":"value"}'` 20 | 21 | With this, you can test integrations and dig deeper. I suggest placing the generated curl in every error handling case dealing with an http request. 22 | -------------------------------------------------------------------------------- /gencurl.go: -------------------------------------------------------------------------------- 1 | package gencurl 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "net" 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | ) 12 | 13 | // FromRequest generates a curl command that can be used when debugging issues 14 | // encountered while executing requests. Be sure to capture your curl before 15 | // you execute the request if you want to capture the post body. 16 | func FromRequest(r *http.Request) string { 17 | ret := fmt.Sprintf("curl -v -X %s %s %s %s %s %s", 18 | r.Method, 19 | getHeaders(r.Header, r.Host), 20 | ifSet(r.UserAgent(), fmt.Sprintf("--user-agent '%s'", r.UserAgent())), 21 | ifSet(r.Referer(), fmt.Sprintf("--referrer '%s'", r.Referer())), 22 | r.URL.String(), 23 | getRequestBody(r)) 24 | 25 | return ret 26 | } 27 | 28 | // FromParams is less useful than FromRequest because the structure of the 29 | // request is likely in the http package call. Unlike a request value, we 30 | // cannot use a response value as we do not have access to the url, method, 31 | // or request body. If you used http.Post(), add a content-type header set to 32 | // the bodyType parameter. If you used http.PostForm(), your content-type is set 33 | // to "application/x-www-form-urlencoded". 34 | func FromParams(method string, urlStr string, requestBody string, headers http.Header) string { 35 | ret := fmt.Sprintf("curl -v -X %s %s %s %s", 36 | method, 37 | getHeaders(headers, extractHost(urlStr)), 38 | urlStr, 39 | ifSet(requestBody, fmt.Sprintf("-d '%s'", requestBody))) 40 | 41 | return ret 42 | } 43 | 44 | func extractHost(urlStr string) string { 45 | u, err := url.Parse(urlStr) 46 | if err != nil { 47 | return "" 48 | } 49 | host, _, _ := net.SplitHostPort(u.Host) 50 | return host 51 | } 52 | 53 | func ifSet(condition string, passThrough string) string { 54 | if len(condition) == 0 { 55 | return "" 56 | } 57 | return passThrough 58 | } 59 | 60 | func getRequestBody(r *http.Request) string { 61 | if r == nil || r.Body == nil { 62 | return "" 63 | } 64 | b, err := ioutil.ReadAll(r.Body) 65 | if err != nil { 66 | return "" 67 | } 68 | 69 | // copy and replace the reader 70 | readerCopy := ioutil.NopCloser(bytes.NewReader(b)) 71 | readerReplace := ioutil.NopCloser(bytes.NewReader(b)) 72 | r.Body = readerReplace 73 | 74 | data, err := ioutil.ReadAll(readerCopy) 75 | if err != nil { 76 | return "" 77 | } 78 | 79 | if len(data) == 0 { 80 | return "" 81 | } 82 | 83 | return fmt.Sprintf("-d '%s'", string(data)) 84 | } 85 | 86 | func getHeaders(h http.Header, Host string) string { 87 | ret := "" 88 | for header, values := range h { 89 | for _, value := range values { 90 | if strings.ToLower(header) != "host" { 91 | ret += fmt.Sprintf(" --header '%s: %v'", header, value) 92 | } 93 | } 94 | } 95 | // the request object does not allow overriding of the host header. one must say req.Host = "foo.bar" 96 | if Host != "" { 97 | ret += fmt.Sprintf(" --header '%s: %v'", "host", Host) 98 | } 99 | 100 | return ret 101 | } 102 | -------------------------------------------------------------------------------- /gencurl_test.go: -------------------------------------------------------------------------------- 1 | package gencurl 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestFromRequest(t *testing.T) { 14 | urlStr := "http://example.com" 15 | data := []byte(`{"key":"value"}`) 16 | body := bytes.NewBuffer(data) 17 | method := "POST" 18 | req, err := http.NewRequest(method, urlStr, body) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | headerContentType := "Content-Type" 24 | contentType := "application/json" 25 | 26 | headerXCustom := "X-Custom" 27 | xCustom1 := `{"json":"data"}` 28 | xCustom2 := "more data" 29 | 30 | req.Header.Add(headerContentType, contentType) 31 | req.Header.Add(headerXCustom, xCustom1) 32 | req.Header.Add(headerXCustom, xCustom2) 33 | 34 | curl := FromRequest(req) 35 | t.Log("Generated Curl: " + curl) 36 | 37 | // be sure to capture your generated curl from your request before 38 | // executing the request if you want to capture the post body as the 39 | // execution of the request will drain the reader for the post body 40 | 41 | /* 42 | c := http.Client{} 43 | resp, err := c.Do(req) 44 | if err != nil { 45 | t.Fatalf("unable to process http request - %s", err) 46 | } 47 | defer resp.Body.Close() 48 | */ 49 | 50 | if want := fmt.Sprintf("-X %s", method); !strings.Contains(curl, want) { 51 | t.Errorf("missing ", want) 52 | } 53 | if want := fmt.Sprintf("--header '%s: %s'", headerContentType, contentType); !strings.Contains(curl, want) { 54 | t.Errorf("missing ", want) 55 | } 56 | if want := fmt.Sprintf("--header '%s: %s'", headerXCustom, xCustom1); !strings.Contains(curl, want) { 57 | t.Errorf("missing ", want) 58 | } 59 | if want := fmt.Sprintf("--header '%s: %s'", headerXCustom, xCustom2); !strings.Contains(curl, want) { 60 | t.Errorf("missing ", want) 61 | } 62 | if want := fmt.Sprintf("-d '%s'", string(data)); !strings.Contains(curl, want) { 63 | t.Errorf("missing ", want) 64 | } 65 | 66 | // Check the body was not emptied 67 | bytes, err := ioutil.ReadAll(req.Body) 68 | if err != nil { 69 | t.Errorf("expected no errors reading body, got %s", err) 70 | } 71 | if len(bytes) == 0 { 72 | t.Errorf("expected body to not be drained.") 73 | } 74 | } 75 | 76 | func TestFromParams(t *testing.T) { 77 | urlStr := "http://www.example.com" 78 | data := url.Values{"key": {"value"}} 79 | _, err := http.PostForm(urlStr, data) 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | curl := FromParams("POST", urlStr, data.Encode(), http.Header{}) 84 | t.Log(curl) 85 | 86 | if want := fmt.Sprintf("-X POST"); !strings.Contains(curl, want) { 87 | t.Errorf("missing %s", want) 88 | } 89 | } 90 | 91 | func TestFromParamsWithNoDataNoHeaders(t *testing.T) { 92 | urlStr := "http://www.example.com" 93 | curl := FromParams("GET", urlStr, "", nil) 94 | t.Log(curl) 95 | 96 | if want := fmt.Sprintf("-X GET"); !strings.Contains(curl, want) { 97 | t.Errorf("missing %s", want) 98 | } 99 | } 100 | 101 | func TestFromParamsWithHeaders(t *testing.T) { 102 | urlStr := "http://www.example.com" 103 | data := url.Values{"key": {"value"}} 104 | _, err := http.PostForm(urlStr, data) 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | curl := FromParams("POST", urlStr, data.Encode(), http.Header{"Content-Type": []string{"application/json"}}) 109 | t.Log(curl) 110 | 111 | if want := fmt.Sprintf("-X POST"); !strings.Contains(curl, want) { 112 | t.Errorf("missing %s", want) 113 | } 114 | if want := fmt.Sprintf("--header 'Content-Type: application/json'"); !strings.Contains(curl, want) { 115 | t.Errorf("missing %s", want) 116 | } 117 | } 118 | --------------------------------------------------------------------------------