Skip to content

fix: improve Linux fzf IPC compatibility#1

Closed
Timmy6942025 wants to merge 5 commits intomasterfrom
fix/linux-fzf-compat
Closed

fix: improve Linux fzf IPC compatibility#1
Timmy6942025 wants to merge 5 commits intomasterfrom
fix/linux-fzf-compat

Conversation

@Timmy6942025
Copy link
Copy Markdown
Owner

Fixes real-time fuzzy search on Linux by adding curl retries, proper HTTP/1.1 framing for nc fallback, and bumping fzf version check from 0.42.0 to 0.48.0.

Timmy added 5 commits March 21, 2026 12:41
- Add curl retry loop (3 attempts, 50ms backoff) before falling back to nc
- Restore HTTP/1.1 framing for nc fallback path
- Retry each nc binary 3 times before moving to next
- Use timeout-wrapped nc as final fallback for Linux distros
- Bump fzf version check from flag-only to >= 0.48.0 for change:reload: support
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 21, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d3d51898-b70a-47cc-a6d0-99e632e96545

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can approve the review once all CodeRabbit's comments are resolved.

Enable the reviews.request_changes_workflow setting to automatically approve the review once all CodeRabbit's comments are resolved.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/fpf/cli_runtime.go`:
- Around line 833-834: The fzf minimum-version check in the change:reload gate
is too low; update the call to checkFzfVersionMin used where change:reload is
enforced (the return currently uses "0.42.0") to require "0.56.1" instead so fzf
>= 0.56.1 is enforced (this ensures the --listen + change:reload race fix is
present). Locate the return using checkFzfVersionMin in the change:reload
handling and replace the version string "0.42.0" with "0.56.1".

In `@cmd/fpf/dynamic_reload.go`:
- Around line 247-257: The retry loop around performReloadHandshake(conn,
config) stores failures into lastErr but still returns lastErr after a later
successful attempt, producing stale error reports; update the loop so that when
performReloadHandshake succeeds you return nil immediately (or explicitly reset
lastErr to nil before returning), and only set lastErr on error cases (in
function performReloadHandshake and the loop surrounding it) so successful
handshakes don't return a previous error.

In `@cmd/fpf/ipc_actions.go`:
- Around line 60-73: The runCurlReload function currently ignores errors from
cmd.Start(), can dereference a nil cmd.Process, and always returns nil so
callers (like runIPCReload) can't detect failures; update runCurlReload to
capture the error returned by cmd.Start(), if err != nil return that error
immediately, only call cmd.Process.Release() after verifying cmd.Process != nil,
and return nil only on success so the caller can fallback on failure. Also
ensure any resources are cleaned on Start error and propagate the error up
rather than swallowing it.
- Around line 76-94: runNetcatReload currently sends raw "reload ...\n" over
TCP; change it to construct a proper HTTP/1.1 POST request with headers and
Content-Length and send that over netcat. Locate runNetcatReload and use
hostPort[0] and hostPort[1] (from strings.Split(host, ":")) to connect, compute
body := fmt.Sprintf("reload(%s)", query) (or "reload "+query if parentheses are
not desired), then build request := fmt.Sprintf("POST / HTTP/1.1\r\nHost:
%s\r\nContent-Length: %d\r\n\r\n%s", hostPort[0], len(body), body) and send that
via netcat/timeoutCmd; remove or repurpose the unused timeoutCmd variable if
necessary. Ensure CRLFs are used for headers and Content-Length matches the byte
length of the body.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4edc5c0a-a120-4d63-8cd6-2d57f7bc389e

📥 Commits

Reviewing files that changed from the base of the PR and between c96c153 and 5fa2f6c.

📒 Files selected for processing (5)
  • cmd/fpf/cli_runtime.go
  • cmd/fpf/dynamic_reload.go
  • cmd/fpf/ipc_actions.go
  • fpf
  • package.json
📜 Review details
🔇 Additional comments (1)
package.json (1)

3-3: Version bump looks correct for this release scope.

Comment thread cmd/fpf/cli_runtime.go
Comment on lines +833 to +834
// change:reload: requires fzf >= 0.42.0
return checkFzfVersionMin("0.42.0")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What is the minimum fzf version required for stable --listen+change:reload behavior used by external IPC reload requests?

💡 Result:

Minimum fzf v0.56.1.

Reason: v0.56.1 fixed a race condition that could leave “stale results after reload, which directly affects setups that combine change:reload (frequent reloads) with external --listen IPC POST reload requests. [1]
(--listen itself (HTTP server accepting POST action sequences like reload(...)) has existed since v0.36.0.) [2]

Sources:
[1] (github.com)
[2] (github.com)

Citations:


Minimum fzf version gate is incorrect and too permissive.

Line 834 uses 0.42.0, but stable --listen + change:reload behavior requires fzf >= 0.56.1. This version fixed a race condition that could leave stale results after reload, which directly impacts external IPC reload requests. The current gate would allow incompatible versions and break dynamic reload at runtime.

🔧 Proposed fix
-	// change:reload: requires fzf >= 0.42.0
-	return checkFzfVersionMin("0.42.0")
+	// change:reload: requires fzf >= 0.56.1 (race condition fix)
+	return checkFzfVersionMin("0.56.1")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/fpf/cli_runtime.go` around lines 833 - 834, The fzf minimum-version check
in the change:reload gate is too low; update the call to checkFzfVersionMin used
where change:reload is enforced (the return currently uses "0.42.0") to require
"0.56.1" instead so fzf >= 0.56.1 is enforced (this ensures the --listen +
change:reload race fix is present). Locate the return using checkFzfVersionMin
in the change:reload handling and replace the version string "0.42.0" with
"0.56.1".

Comment thread cmd/fpf/dynamic_reload.go
Comment on lines +247 to +257
for attempt := 1; attempt <= maxReloadAttempts; attempt++ {
if err := performReloadHandshake(conn, config); err != nil {
lastErr = err
if attempt < maxReloadAttempts {
sleepDuration := time.Duration(attempt*attempt*50) * time.Millisecond
time.Sleep(sleepDuration)
continue
}
}
return lastErr
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Retry loop returns a stale error even after a successful retry.

After any earlier failure, lastErr remains set. A later successful handshake still returns that old error.

🔧 Proposed fix
 func handleIPCReload(conn net.Conn, config any) error {
 	var lastErr error
 	for attempt := 1; attempt <= maxReloadAttempts; attempt++ {
 		if err := performReloadHandshake(conn, config); err != nil {
 			lastErr = err
 			if attempt < maxReloadAttempts {
 				sleepDuration := time.Duration(attempt*attempt*50) * time.Millisecond
 				time.Sleep(sleepDuration)
 				continue
 			}
-		}
-		return lastErr
+			return lastErr
+		}
+		return nil
 	}
 	return lastErr
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for attempt := 1; attempt <= maxReloadAttempts; attempt++ {
if err := performReloadHandshake(conn, config); err != nil {
lastErr = err
if attempt < maxReloadAttempts {
sleepDuration := time.Duration(attempt*attempt*50) * time.Millisecond
time.Sleep(sleepDuration)
continue
}
}
return lastErr
}
for attempt := 1; attempt <= maxReloadAttempts; attempt++ {
if err := performReloadHandshake(conn, config); err != nil {
lastErr = err
if attempt < maxReloadAttempts {
sleepDuration := time.Duration(attempt*attempt*50) * time.Millisecond
time.Sleep(sleepDuration)
continue
}
return lastErr
}
return nil
}
return lastErr
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/fpf/dynamic_reload.go` around lines 247 - 257, The retry loop around
performReloadHandshake(conn, config) stores failures into lastErr but still
returns lastErr after a later successful attempt, producing stale error reports;
update the loop so that when performReloadHandshake succeeds you return nil
immediately (or explicitly reset lastErr to nil before returning), and only set
lastErr on error cases (in function performReloadHandshake and the loop
surrounding it) so successful handshakes don't return a previous error.

Comment thread cmd/fpf/ipc_actions.go
Comment on lines +60 to +73
func runCurlReload(host, query string) error {
// fzf 0.42+ supports change:reload: which sends HTTP/1.1 POST
url := "http://" + host + "/fzf_reload"
payload := "reload " + query

reloadCmd := buildDynamicReloadCommandForQuery(managerOverride, fallbackFile, managerListCSV, query, bypassQueryCache)
actionPayload := "change-prompt(Search> )+reload(" + reloadCmd + ")"
cmd := exec.Command("curl", "-s", "-X", "POST", "-d", payload,
"--max-time", "1",
"-H", "Content-Type: application/octet-stream",
url)

return sendFzfListenAction(actionPayload)
// Run without waiting — fire and forget, fzf handles it
cmd.Start()
cmd.Process.Release()
return nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Curl path can panic and falsely report success, preventing fallback.

Line 71 ignores Start() errors, Line 72 can dereference a nil Process, and the function always returns nil. That makes runIPCReload treat failed sends as success.

🔧 Proposed fix
 func runCurlReload(host, query string) error {
 	// fzf 0.42+ supports change:reload: which sends HTTP/1.1 POST
 	url := "http://" + host + "/fzf_reload"
 	payload := "reload " + query

 	cmd := exec.Command("curl", "-s", "-X", "POST", "-d", payload,
 		"--max-time", "1",
+		"--fail",
 		"-H", "Content-Type: application/octet-stream",
 		url)

-	// Run without waiting — fire and forget, fzf handles it
-	cmd.Start()
-	cmd.Process.Release()
-	return nil
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("curl reload failed: %w", err)
+	}
+	return nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func runCurlReload(host, query string) error {
// fzf 0.42+ supports change:reload: which sends HTTP/1.1 POST
url := "http://" + host + "/fzf_reload"
payload := "reload " + query
reloadCmd := buildDynamicReloadCommandForQuery(managerOverride, fallbackFile, managerListCSV, query, bypassQueryCache)
actionPayload := "change-prompt(Search> )+reload(" + reloadCmd + ")"
cmd := exec.Command("curl", "-s", "-X", "POST", "-d", payload,
"--max-time", "1",
"-H", "Content-Type: application/octet-stream",
url)
return sendFzfListenAction(actionPayload)
// Run without waiting — fire and forget, fzf handles it
cmd.Start()
cmd.Process.Release()
return nil
func runCurlReload(host, query string) error {
// fzf 0.42+ supports change:reload: which sends HTTP/1.1 POST
url := "http://" + host + "/fzf_reload"
payload := "reload " + query
cmd := exec.Command("curl", "-s", "-X", "POST", "-d", payload,
"--max-time", "1",
"--fail",
"-H", "Content-Type: application/octet-stream",
url)
if err := cmd.Run(); err != nil {
return fmt.Errorf("curl reload failed: %w", err)
}
return nil
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/fpf/ipc_actions.go` around lines 60 - 73, The runCurlReload function
currently ignores errors from cmd.Start(), can dereference a nil cmd.Process,
and always returns nil so callers (like runIPCReload) can't detect failures;
update runCurlReload to capture the error returned by cmd.Start(), if err != nil
return that error immediately, only call cmd.Process.Release() after verifying
cmd.Process != nil, and return nil only on success so the caller can fallback on
failure. Also ensure any resources are cleaned on Start error and propagate the
error up rather than swallowing it.

Comment thread cmd/fpf/ipc_actions.go
Comment on lines +76 to 94
func runNetcatReload(host, query string) error {
// For older fzf: use netcat to send fzf IPC protocol message
// Protocol: one line per event, \n terminated
// Event format: "event payload\n"
hostPort := strings.Split(host, ":")
if len(hostPort) != 2 {
return fmt.Errorf("invalid FPF_FZF_LISTEN_HOST: %s (expected host:port)", host)
}

return strings.Join(parts, " ")
}
payload := fmt.Sprintf("reload %s\n", query)

func sendFzfListenAction(actionPayload string) error {
fzfPort := strings.TrimSpace(os.Getenv("FZF_PORT"))
if fzfPort == "" {
return fmt.Errorf("missing FZF_PORT")
// Use timeout(1) wrapper for compatibility across Linux/macOS
// timeout on Linux, gtimeout on macOS (coreutils)
timeoutCmd := "timeout"
if _, err := exec.LookPath("timeout"); err != nil {
timeoutCmd = "gtimeout"
}
_ = timeoutCmd // reserved for future netcat variant use

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For fzf --listen, what exact request format is accepted on the listen socket for reload actions (raw action line vs HTTP/1.1 POST /fzf_reload)?

💡 Result:

fzf --listen runs a tiny HTTP server. For triggering a reload you must send an HTTP/1.x POST to / (not a raw “action line” on the socket, and not POST /fzf_reload). The request line must start with POST / HTTP…; GET /?... is used only for the JSON state endpoint. [1]

The POST body is interpreted as an fzf action list string (same syntax as in --bind, e.g. reload(...), reload-sync(...), chained with +). fzf reads Content-Length, then parses the body via parseSingleActionList(strings.Trim(body, "\r\n")), and queues the resulting actions. [2]

Minimal example (TCP):

POST / HTTP/1.1
Host: localhost
Content-Length: 16

reload(ps -ef)

(Optionally include X-API-Key: ... if fzf was started with an API key.) [2][1]

Sources: fzf server.go request parsing / POST handling [1][2]; release notes describing --listen as an HTTP server with GET / JSON state [3].

[1] (github.com)
[2] (github.com)
[3] (junegunn.github.io)

Citations:


HTTP/1.1 POST format required but raw protocol sent instead.

runNetcatReload sends raw reload ...\n over TCP, but fzf's --listen requires HTTP/1.1 POST to / with proper headers and Content-Length. The payload must be HTTP-framed like:

POST / HTTP/1.1
Host: localhost
Content-Length: 16

reload(ps -ef)

The timeout/nc setup is currently unused. Update the payload to use proper HTTP/1.1 formatting before sending over netcat.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/fpf/ipc_actions.go` around lines 76 - 94, runNetcatReload currently sends
raw "reload ...\n" over TCP; change it to construct a proper HTTP/1.1 POST
request with headers and Content-Length and send that over netcat. Locate
runNetcatReload and use hostPort[0] and hostPort[1] (from strings.Split(host,
":")) to connect, compute body := fmt.Sprintf("reload(%s)", query) (or "reload
"+query if parentheses are not desired), then build request := fmt.Sprintf("POST
/ HTTP/1.1\r\nHost: %s\r\nContent-Length: %d\r\n\r\n%s", hostPort[0], len(body),
body) and send that via netcat/timeoutCmd; remove or repurpose the unused
timeoutCmd variable if necessary. Ensure CRLFs are used for headers and
Content-Length matches the byte length of the body.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 21, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 3 file(s) based on 4 unresolved review comments.

A stacked PR containing fixes has been created.

  • Stacked PR: #2
  • Files modified:
  • cmd/fpf/cli_runtime.go
  • cmd/fpf/dynamic_reload.go
  • cmd/fpf/ipc_actions.go

Time taken: 2m 33s

Timmy6942025 added a commit that referenced this pull request Mar 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant