fix URL-decode file:// paths before stat/exists checks

This commit is contained in:
Yiorgis Gozadinos 2026-05-22 13:45:55 +03:00
parent 7ea61a7b10
commit 6d15f1f247
No known key found for this signature in database
2 changed files with 67 additions and 10 deletions

View file

@ -1,7 +1,7 @@
import tempfile
from pathlib import Path
from typing import TYPE_CHECKING
from urllib.parse import urlparse
from urllib.parse import unquote, urlparse
from haiku.rag.client.processing import (
ensure_chunks_embedded,
@ -315,8 +315,10 @@ async def create_document_from_source(
# Directory case: recurse with the existing FS filter and produce one
# document per file. Remote schemes (http/s3) never hit this branch.
if parsed_url.scheme in ("", "file"):
# file:// URIs URL-encode special characters ([, ], spaces, etc.);
# unquote to get the real filesystem path before any stat/rglob.
local_path = (
Path(parsed_url.path)
Path(unquote(parsed_url.path))
if parsed_url.scheme == "file"
else (Path(source) if isinstance(source, str) else source)
)
@ -355,16 +357,15 @@ async def create_document_from_source(
fetcher = resolve_fetcher(source_str, storage_options=storage_options)
# The stored URI is what we look up + persist by. For an explicit uri
# override, use it as-is. Otherwise canonicalize local paths to file://
# and leave remote URIs alone.
# override, use it as-is. For a file:// input the source string is
# already canonical (URL-encoded); round-tripping via Path.as_uri()
# would double-encode any escapes like %5B. For bare paths, canonicalize.
if uri is not None:
stored_uri = uri
elif parsed_url.scheme in ("", "file"):
stored_uri = (
(Path(parsed_url.path) if parsed_url.scheme == "file" else Path(source_str))
.absolute()
.as_uri()
)
elif parsed_url.scheme == "file":
stored_uri = source_str
elif parsed_url.scheme == "":
stored_uri = Path(source_str).absolute().as_uri()
else:
stored_uri = source_str

View file

@ -135,6 +135,62 @@ async def test_e2e_initial_sweep_lands_succeeded_jobs(tmp_path, jobs, sync):
assert row_b is not None and row_b.content_hash and row_b.last_ingested_at
@pytest.mark.asyncio
async def test_e2e_handles_url_encoded_special_chars_in_path(tmp_path, jobs, sync):
"""File names containing characters that path.as_uri() URL-encodes (e.g.
Next.js dynamic-route brackets like `[chunk_id]`) must survive the
round-trip through the job queue without tripping the existence check
inside create_document_from_source."""
bracketed_dir = tmp_path / "[chunk_id]"
bracketed_dir.mkdir()
target = bracketed_dir / "route.ts"
target.write_text("export default {};")
client = _mock_client(tmp_path)
cfg = FSSourceConfig(
type="fs",
id="local",
root=tmp_path,
poll_interval_s=60.0,
)
manager = PollerManager(
configs=[cfg],
job_repo=jobs,
sync_repo=sync,
supported_extensions=[".ts"],
)
pool = WorkerPool(
client=client,
job_repo=jobs,
sync_repo=sync,
worker_count=1,
max_concurrent=1,
poll_idle_interval_s=0.05,
)
await pool.start()
await manager.start()
try:
async def _one_succeeded() -> bool:
counts = await jobs.counts_by_status()
return counts.get("succeeded", 0) == 1
await _wait_for(_one_succeeded, timeout=5.0)
finally:
await manager.stop()
await pool.stop()
counts = await jobs.counts_by_status()
assert counts.get("succeeded", 0) == 1
assert counts.get("dead", 0) == 0 # no PermanentError("File does not exist")
# The URI in the queue is URL-encoded; the worker still finds the file.
[call] = client.create_document_from_source.await_args_list
assert "%5Bchunk_id%5D" in call.args[0]
@pytest.mark.asyncio
async def test_e2e_watchfiles_push_event_lands_as_job(tmp_path, jobs, sync):
"""FSPoller's watchfiles loop: a file *added* after startup should land