Skip to content

Instantly share code, notes, and snippets.

@neomantra
Created February 17, 2026 16:29
Show Gist options
  • Select an option

  • Save neomantra/952a1009c1c66cc64626ad1bc9cc30de to your computer and use it in GitHub Desktop.

Select an option

Save neomantra/952a1009c1c66cc64626ad1bc9cc30de to your computer and use it in GitHub Desktop.
convert HTTPRequest to curl invocation
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