FilesystemFileSearchMiddleware: `grep_search` swallows ripgrep errors and reports "No matches found"; Python fallback never runs and `max_file_size_mb` is ignored on the ripgrep path

#41026 · closed · 6 comments

View on GitHub ↗

sauravsolanki

### Submission checklist - [x] This is a bug, not a usage question. - [x] I added a clear and descriptive title that summarizes this issue. - [x] I used the GitHub search to find a similar question and didn't find it. - [x] I am sure that this is a bug in LangChain rather than my code. - [x] The bug is not resolved by updating to the latest stable version of LangChain (or the specific integration package). - [x] This is not related to the langchain-community package. - [x] I posted a self-contained, minimal, reproducible example. A maintainer can copy it and run it AS IS. ### Package (Required) - [x] langchain - [ ] langchain-openai - [ ] langchain-anthropic - [ ] langchain-classic - [ ] langchain-core - [ ] langchain-model-profiles - [ ] langchain-tests - [ ] langchain-text-splitters - [ ] langchain-chroma - [ ] langchain-deepseek - [ ] langchain-exa - [ ] langchain-fireworks - [ ] langchain-groq - [ ] langchain-huggingface - [ ] langchain-mistralai - [ ] langchain-nomic - [ ] langchain-ollama - [ ] langchain-openrouter - [ ] langchain-perplexity - [ ] langchain-qdrant - [ ] langchain-typesafe - [ ] langchain-xai - [ ] Other / not sure / general ### Related Issues / PRs #40255 (fallback ReDoS) touches the same fallback path but is a different problem. ### Reproduction Steps / Example Code (Python) ```python import subprocess import tempfile from pathlib import Path from langchain.agents.middleware.file_search import FilesystemFileSearchMiddleware d = tempfile.mkdtemp() Path(d, "a.txt").write_text("foobar\nfoobaz\n") # 1. A regex Python accepts but ripgrep rejects (look-ahead) for use_rg in (True, False): mw = FilesystemFileSearchMiddleware(root_path=d, use_ripgrep=use_rg) out = mw.grep_search.invoke({"pattern": "foo(?=bar)", "output_mode": "content"}) print(f"use_ripgrep={use_rg}: {out!r}") r = subprocess.run(["rg", "--json", "--", "foo(?=bar)", d], capture_output=True, text=True) print("rg returncode:", r.returncode, "| stderr:", r.stderr.strip().splitlines()[-1]) # 2. max_file_size_mb is only enforced by the Python fallback Path(d, "big.txt").write_text("needle\n" + "x" * (11 * 1024 * 1024)) for use_rg in (True, False): mw = FilesystemFileSearchMiddleware(root_path=d, use_ripgrep=use_rg, max_file_size_mb=10) print(f"max_file_size_mb=10 use_ripgrep={use_rg}:", mw.grep_search.invoke({"pattern": "needle"})) ``` ### Error Message and Stack Trace (if applicable) ```shell No exception. Output of the script above (ripgrep 13.0.0 on PATH): use_ripgrep=True: 'No matches found' use_ripgrep=False: '/a.txt:1:foobar' rg returncode: 2 | stderr: error: look-around, including look-ahead and look-behind, is not supported max_file_size_mb=10 use_ripgrep=True: /big.txt max_file_size_mb=10 use_ripgrep=False: No matches found Expected: identical results regardless of `use_ripgrep`. Either the tool falls back to the Python search when ripgrep rejects the pattern (so `/a.txt:1:foobar` is returned), or it surfaces ripgrep's error to the model instead of claiming there are no matches. And `max_file_size_mb` should be honoured on both paths. ``` ### Description `FilesystemFileSearchMiddleware.grep_search` (`langchain/agents/middleware/file_search.py`) is documented as using ripgrep when available and falling back to a pure-Python search otherwise. The fallback never runs on a ripgrep *error*, only when the binary is missing or times out: ```python # grep_search results = None if self.use_ripgrep: with suppress(FileNotFoundError, subprocess.CalledProcessError, subprocess.TimeoutExpired): results = self._ripgrep_search(pattern, path, include) if results is None: results = self._python_search(pattern, path, include) if not results: return "No matches found" # _ripgrep_search result = subprocess.run(cmd, capture_output=True, text=True, timeout=30, check=False) ... for line in result.stdout.splitlines(): # stdout is empty on error ... return results # {} , not None ``` `check=False` means a non-zero exit never raises `CalledProcessError`, so `_ripgrep_search` parses an empty stdout and returns `{}`. That is not `None`, so the fallback is skipped and the model is told there are no matches. ripgrep exits with code 2 for any pattern its regex engine rejects: look-ahead/look-behind, backreferences, some Unicode property escapes, etc. These are all valid Python regexes, so a model that writes `(?<=foo)bar` gets a confident "No matches found" for a file that does match, and nothing in the result hints that the search failed. `result.stderr`, which contains the exact reason, is discarded. A second divergence in the same function: `max_file_size_mb` is checked in `_python_search` (`file_path.stat().st_size > self.max_file_size_bytes`) but the ripgrep command is built without `--max-filesize`, so the two paths return different results for the same inputs (see the 11 MB file in the repro). **Proposed fix** In `_ripgrep_search`, after `subprocess.run`: ```python if result.returncode not in (0, 1): # 0 = matches, 1 = no matches, 2 = error return None # let grep_search fall back to the Python search ``` (or, alternatively, return a message built from `result.stderr` so the model sees "Invalid regex pattern for ripgrep: look-around ... is not supported" and can rewrite the pattern), and add `"--max-filesize", str(self.max_file_size_bytes)` to `cmd` so the size limit applies on both paths. Both are a few lines plus tests; happy to open the PR if assigned. ### System Info Reproduced on langchain master (57236d5) and on the released packages below. ripgrep 13.0.0. System Information ------------------ > OS: Linux > OS Version: #204-Ubuntu SMP Wed Sep 2 13:09:23 UTC 2026 > Python Version: 3.13.11 (main, Jan 14 2026, 19:38:04) [Clang 21.1.4 ] Package Information ------------------- > langchain_core: 1.6.3 > langchain: 1.4.0 > langsmith: 0.8.18 > langgraph: 1.2.11 > pydantic: 2.12.5 ### Social handles (optional) LinkedIn: https://linkedin.com/in/sauravsolanki GitHub: https://github.com/sauravsolanki

Comments

FaizanArif15

I reproduced both problems on current master (commit `57236d55d9`, langchain `1.4.3`, ripgrep 14.1.0, Linux). 1. `foo(?=bar)` with `use_ripgrep=True` returns "No matches found", while `use_ripgrep=False` returns `/a.txt:1:foobar`. `rg` exits with code 2 (look-around is not supported). As the issue notes, this appears to be because `_ripgrep_search` returns an empty result rather than `None`, so the Python fallback never runs. 2. With `max_file_size_mb=10`, an 11 MB file is returned on the ripgrep path but skipped on the Python path. ``` use_ripgrep=True: 'No matches found' use_ripgrep=False: '/a.txt:1:foobar' rg returncode: 2 max_file_size_mb=10 use_ripgrep=True: /big.txt max_file_size_mb=10 use_ripgrep=False: No matches found ``` One design question for the maintainers: when ripgrep errors, should `grep_search` fall back to the Python search, or return ripgrep's stderr to the model so it can rewrite the pattern? @sauravsolanki you mentioned you'd open the PR once assigned. I'm happy to help with testing or review, or to take it if you'd rather not.

SIDDARTHAREDDY8

I can take this on - could I be assigned to #41026? My approach, verified against the repro above: 1. When ripgrep exits with anything other than 0 (matches) or 1 (no matches), `_ripgrep_search` now returns `None` instead of `{}`. The caller already falls back to the Python search when it gets `None`, so an `rg` error (e.g. exit 2 on look-around) now runs the documented fallback instead of printing "No matches found". I went with the fallback over surfacing stderr to the model since the method already documents a Python fallback and the `FileNotFoundError`/`TimeoutExpired` branches fall back the same way. 2. `max_file_size_mb` was ignored on the ripgrep path - added `--max-filesize` to the `rg` command so both paths honor it. Verified locally with ripgrep 14.1.0: `foo(?=bar)` now returns the Python-search result on the ripgrep path, and an 11MB file is skipped by both paths with `max_file_size_mb=10`. Three regression tests added (error falls back to Python search, `None` on rg error, `--max-filesize` in the command), all fail on the original code and pass with the fix; full file-search suite green (45 passed), ruff + mypy strict clean. I'll open a PR shortly.

SIDDARTHAREDDY8

Hi, I'd like to take this on. The bug: grep_search shells out to ripgrep and fails hard when ripgrep errors or is unavailable. My approach falls back to a pure-Python search when the ripgrep call fails, so the tool degrades instead of erroring. I already have a fix implemented in #41033, but it was auto-closed by the require-issue-link check since I'm not assigned to this issue. Could I please be assigned so I can reopen it?

0xamlab

I reproduced this on master. With `use_ripgrep=True`, `foo(?=bar)` makes `rg` exit 2, but `_ripgrep_search` uses `check=False` at `libs/langchain_v1/langchain/agents/middleware/file_search.py:314` and returns `{}` at `:347`, so `grep_search` reports "No matches found" — the same string as a genuine empty result. The fallback approach in #41033 (returning `None` on non-0/1 exit codes so the Python search runs) fixes it: the repro returns `/a.txt:1:foobar` and the file-search suite is green (45/45). Surfacing `stderr` distinctly would be a nice follow-up, but the fallback matches the existing docstring promise. Disclosure: I contribute to thyn-ai/Algenta (open-source agent tooling).

kpdagrt22

Maintainers ? I implemented and locally verified the two fixes reported here: - Treat ripgrep exit codes other than 0 (match) and 1 (no match) as a failed ripgrep search, so `grep_search` uses its existing Python fallback instead of reporting a false ?No matches found?. - Pass `--max-filesize` using `max_file_size_bytes` so the configured limit applies on the ripgrep path too. I added regression coverage for the exit-code-2 fallback and asserted the size-limit argument. Ruff and format checks pass; a direct runtime check passes for both cases. The focused pytest file cannot currently collect in this environment because `blockbuster` is missing from the configured interpreter. I see that @SIDDARTHAREDDY8 already prepared equivalent changes in PR #41033, which was automatically closed because they were not assigned. To avoid duplicating their work, would you prefer to assign them and reopen #41033, or assign me to submit this equivalent implementation? I will wait for maintainer direction and assignment before opening a PR.

kpdagrt22

Following up on my proposed fix: I have the locally verified changes ready. Since PR #41033 was closed pending assignment, I’ll continue to hold off on opening a PR. If maintainers approve this approach and would like me to submit it, please assign me first; otherwise I’m happy to defer to another contributor.