Open-Source Wikis

/

fzf

/

Systems

/

HTTP server (`--listen`)

junegunn/fzf

HTTP server (--listen)

Purpose

--listen [ADDR] puts fzf into a control-plane mode where external programs can push actions over HTTP (or a Unix domain socket). This is what enables building rich pickers that update fzf's preview, prompt, query, or input list from outside the process.

File

File Purpose
src/server.go The whole server: listener setup, request parsing, request routing.
src/terminal.go Hosts the actionChannel that the server sends parsed action lists into, and the GET handler that returns the current state.

Key abstractions

Symbol Description
httpServer (in src/server.go) Wraps the API key, the action channel, and the GET handler.
listenAddress Parsed [host:]port or *.sock path. IsLocal() returns true for localhost, 127.0.0.1, or any Unix socket.
parseListenAddress(string) Permissive parser; defaults to localhost / random port.
startHttpServer(addr, actionChannel, getHandler) Spawns the listener and the accept loop. Returns the net.Listener, the bound port, and an error.
getRegex (regex) Matches GET / (with optional query string) requests.
parseGetParams(query) Extracts limit and offset for paginated GETs.

How it works

graph LR
    CLIENT[curl / external program] -->|HTTP POST or GET| LISTENER[net.Listener]
    LISTENER --> SERVER[httpServer.handleHttpRequest]
    SERVER -->|GET /?limit=...&offset=...| HANDLER[getHandler in terminal.go]
    SERVER -->|POST / body=action| PARSE[parseSingleActionList]
    PARSE -->|actionChannel| TERM[Terminal action engine]
    TERM -->|state changes| RENDERER

Listener setup

startHttpServer chooses between TCP and Unix sockets based on the address. For *.sock paths it removes a stale socket if one exists (after probing it with a net.Dial to confirm nothing is listening), creates the new socket, and chmods it 0600. For TCP, the address must be localhost (or 127.0.0.1) unless FZF_API_KEY is set, otherwise the call fails before binding.

Request parsing

The server intentionally does not import net/http. The comment in server.go documents the binary-size impact:

* No --listen:            2.8MB
* --listen with net/http: 5.7MB
* --listen w/o net/http:  3.3MB

Instead, handleHttpRequest uses a bufio.Scanner with a custom split function to walk request lines, parses headers manually, validates Content-Length (capped at maxContentLength = 1 MiB) and X-API-Key, and reads the body up to the declared length. It supports two methods:

  • GET / (optionally with ?limit=...&offset=...) — returns the current match list as JSON. Implemented by getHandler, which lives in terminal.go and snapshots the current merger.
  • POST / — body is a single fzf action list (the same syntax as --bind action arguments, e.g. change-prompt(picked> )+reload(ls)). The body is parsed via parseSingleActionList, then pushed onto actionChannel (with a channelTimeout = 2s so a stuck terminal returns 503 instead of blocking forever).

API key

If FZF_API_KEY is set, every request must include X-API-Key: <value>. The comparison uses subtle.ConstantTimeCompare to avoid timing leaks. For non-localhost binds the API key is mandatory; for localhost or Unix-socket binds it is optional.

Response shape

  • 200 OK with Content-Type: application/json for successful GETs (the body is fzf's own JSON encoding of the visible items).
  • 200 OK empty for successful POSTs.
  • 400 Bad Request for malformed requests, invalid action syntax, or oversized bodies.
  • 401 Unauthorized for bad API keys.
  • 503 Service Unavailable if the action queue is full or the GET handler timed out.

Integration points

  • Owned by terminal.go: the server is started during NewTerminal when --listen is set. The terminal feeds the server an actionChannel and getHandler closure that snapshots the current Merger.
  • Action engine: actions arriving over HTTP go through the same dispatch as keyboard-triggered ones. There is no special-case path for --listen.
  • Cleanup: the listener is closed via defer from terminal cleanup; the listener's Accept loop exits gracefully when it sees net.ErrClosed.

Entry points for modification

  • Adding a new HTTP method: branch in handleHttpRequest. Keep the implementation tight — don't pull in net/http; the savings are part of fzf's identity.
  • Changing the GET response: edit getHandler in src/terminal.go. Be careful with parseGetParams defaults; consumers (notably the README examples) rely on limit=100.
  • Action injection from internal code: push directly onto actionChannel. This is the same channel HTTP requests target.

Example

# Terminal A
seq 100 | fzf --listen 8123

# Terminal B
curl -X POST -d 'change-prompt(custom> )+change-query(42)' localhost:8123
curl 'localhost:8123/?limit=5'

The first curl mutates the running fzf's state; the second returns the first 5 items currently in the visible match list as JSON.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

HTTP server (`--listen`) – fzf wiki | Factory