Created
February 17, 2026 16:29
-
-
Save neomantra/952a1009c1c66cc64626ad1bc9cc30de to your computer and use it in GitHub Desktop.
convert HTTPRequest to curl invocation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package curl | |
| import ( | |
| "bytes" | |
| "fmt" | |
| "io" | |
| "net/http" | |
| "strconv" | |
| "strings" | |
| "unicode/utf8" | |
| ) | |
| // RequestToCurl converts an http.Request to a copy-pasteable curl command. | |
| // The request body is restored after reading so the request can still be executed. | |
| func RequestToCurl(req *http.Request) string { | |
| if req == nil { | |
| return "" | |
| } | |
| var parts []string | |
| parts = append(parts, "curl") | |
| // Method (curl defaults to GET, but explicit is clearer for humans) | |
| if req.Method != "" && req.Method != "GET" { | |
| parts = append(parts, "-X", req.Method) | |
| } | |
| // URL (always quoted for safety) | |
| parts = append(parts, shellescape(req.URL.String())) | |
| // Headers | |
| // Sort keys for deterministic output (optional, but nice for testing) | |
| for key, values := range req.Header { | |
| for _, value := range values { | |
| // Skip default curl headers unless explicitly set | |
| if key == "Accept-Encoding" && value == "gzip" { | |
| // Use --compressed shorthand for better readability | |
| continue | |
| } | |
| header := fmt.Sprintf("%s: %s", key, value) | |
| parts = append(parts, "-H", shellescape(header)) | |
| } | |
| } | |
| // Handle compression flag if gzip was requested | |
| if req.Header.Get("Accept-Encoding") == "gzip" { | |
| parts = append(parts, "--compressed") | |
| } | |
| // Body handling | |
| if req.Body != nil && req.Body != http.NoBody { | |
| bodyBytes, err := io.ReadAll(req.Body) | |
| if err == nil && len(bodyBytes) > 0 { | |
| // Restore body so the request remains usable | |
| req.Body = io.NopCloser(bytes.NewReader(bodyBytes)) | |
| // Handle binary vs text content | |
| if isBinary(bodyBytes) { | |
| // For binary, use @filename notation or base64, but since we don't have files, | |
| // we'll use --data-binary with a warning comment | |
| parts = append(parts, "--data-binary", shellescape(string(bodyBytes))) | |
| } else { | |
| // Text content - check if we should use @- for stdin or inline | |
| if len(bodyBytes) > 2000 { | |
| // For large payloads, suggest heredoc syntax in comment | |
| parts = append(parts, "-d", shellescape(string(bodyBytes))) | |
| } else { | |
| parts = append(parts, "-d", shellescape(string(bodyBytes))) | |
| } | |
| } | |
| } else { | |
| req.Body = io.NopCloser(bytes.NewReader(bodyBytes)) | |
| } | |
| } | |
| // Format with line continuation for readability | |
| return formatMultiline(parts) | |
| } | |
| // shellescape wraps a string in single quotes, escaping any single quotes inside. | |
| // This is POSIX-compliant shell escaping. | |
| func shellescape(s string) string { | |
| if s == "" { | |
| return "''" | |
| } | |
| // Replace ' with '\'' | |
| return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'" | |
| } | |
| // isBinary heuristically determines if data is binary (non-text) | |
| func isBinary(data []byte) bool { | |
| if !utf8.Valid(data) { | |
| return true | |
| } | |
| // Check for high ratio of control characters | |
| if len(data) == 0 { | |
| return false | |
| } | |
| controlChars := 0 | |
| for _, b := range data { | |
| if b < 32 && b != '\n' && b != '\r' && b != '\t' { | |
| controlChars++ | |
| } | |
| } | |
| return float64(controlChars)/float64(len(data)) > 0.3 | |
| } | |
| // formatMultiline formats the command with backslash continuation | |
| func formatMultiline(parts []string) string { | |
| if len(parts) <= 2 { | |
| return strings.Join(parts, " ") | |
| } | |
| var result strings.Builder | |
| result.WriteString(parts[0]) | |
| result.WriteString(" \\\n") | |
| // Indent subsequent lines | |
| for i, part := range parts[1:] { | |
| result.WriteString(" ") | |
| result.WriteString(part) | |
| if i < len(parts)-2 { | |
| result.WriteString(" \\\n") | |
| } | |
| } | |
| return result.String() | |
| } | |
| // RequestToCurlCompact returns a single-line version for constrained spaces | |
| func RequestToCurlCompact(req *http.Request) string { | |
| if req == nil { | |
| return "" | |
| } | |
| cmd := RequestToCurl(req) | |
| // Remove line continuations and extra spaces | |
| cmd = strings.ReplaceAll(cmd, "\\\n", "") | |
| cmd = strings.Join(strings.Fields(cmd), " ") | |
| return cmd | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment