package utils import ( "crypto/rand" "encoding/hex" "encoding/json" "fmt" "net/http" "os" "path/filepath" "time" ) const ( debugDir = "failed_requests" maxSaveBody = 5 * 1024 * 1024 // 5 MiB, configurable filePerm = 0640 dirPerm = 0750 timestampFormat = "20060102T150405Z" // safe for filenames ) // helper: generate a small random hex id func randomHex(n int) string { b := make([]byte, n) _, _ = rand.Read(b) return hex.EncodeToString(b) } // save a failing request atomically with metadata func saveFailedRequest(prefix, url string, reqHeaders http.Header, respHeaders http.Header, status int, body []byte, errMsg string) (string, error) { if err := os.MkdirAll(debugDir, dirPerm); err != nil { return "", fmt.Errorf("mkdir debug dir: %w", err) } ts := time.Now().UTC().Format(timestampFormat) id := randomHex(6) base := fmt.Sprintf("%s_%s_%s", prefix, ts, id) // Truncate body if too large truncated := false if len(body) > maxSaveBody { body = body[:maxSaveBody] truncated = true } meta := map[string]interface{}{ "timestamp_utc": ts, "url": url, "status": status, "error": errMsg, "truncated": truncated, "req_headers": reqHeaders, "resp_headers": respHeaders, } metaBytes, _ := json.MarshalIndent(meta, "", " ") metaTmp := filepath.Join(debugDir, base+".meta.json.tmp") bodyTmp := filepath.Join(debugDir, base+".body.tmp") metaPath := filepath.Join(debugDir, base+".meta.json") bodyPath := filepath.Join(debugDir, base+".body") // write temp + rename (atomic on POSIX filesystems) if err := os.WriteFile(metaTmp, metaBytes, filePerm); err != nil { return "", fmt.Errorf("write meta tmp: %w", err) } if err := os.Rename(metaTmp, metaPath); err != nil { return "", fmt.Errorf("rename meta: %w", err) } if err := os.WriteFile(bodyTmp, body, filePerm); err != nil { return "", fmt.Errorf("write body tmp: %w", err) } if err := os.Rename(bodyTmp, bodyPath); err != nil { return "", fmt.Errorf("rename body: %w", err) } return base, nil }