Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion langfuse/_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3744,7 +3744,7 @@ def _url_encode(self, url: str, *, is_url_param: Optional[bool] = False) -> str:
# “%”, “?”, “#”, “|”, … in query/path parts). Re-quoting here would
# double-encode, so we skip when the value is about to be sent straight
# to httpx (`is_url_param=True`) and the installed version is ≥ 0.28.
if is_url_param and Version(httpx.__version__) >= Version("0.28.0"):
if is_url_param:
return url

# urllib.parse.quote does not escape slashes "/" by default; we need to add safe="" to force escaping
Expand Down
94 changes: 94 additions & 0 deletions tests/unit/test_dataset_url_encoding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import pytest
from unittest.mock import patch, MagicMock
from langfuse import Langfuse
import httpx

def test_dataset_url_encoding_in_requests():
with patch("httpx.Client.send") as mock_send:
# Mock response for get
mock_response = MagicMock(spec=httpx.Response)
mock_response.status_code = 200
mock_response.json.return_value = {
"id": "dataset-id",
"name": "my/dataset",
"description": "test",
"metadata": {},
"projectId": "project-id",
"createdAt": "2026-01-01T00:00:00Z",
"updatedAt": "2026-01-01T00:00:00Z"
}
mock_response.headers = httpx.Headers()

# Mock response for list
mock_items_response = MagicMock(spec=httpx.Response)
mock_items_response.status_code = 200
mock_items_response.json.return_value = {
"data": [],
"meta": {"page": 1, "limit": 50, "totalItems": 0, "totalPages": 1}
}

# Mock response for run
mock_run_response = MagicMock(spec=httpx.Response)
mock_run_response.status_code = 200
mock_run_response.json.return_value = {
"id": "run-id",
"name": "my/run",
"datasetName": "my/dataset",
"datasetId": "dataset-id",
"createdAt": "2026-01-01T00:00:00Z",
"updatedAt": "2026-01-01T00:00:00Z",
"metadata": {},
"datasetRunItems": []
}

# Mock response for runs
mock_runs_response = MagicMock(spec=httpx.Response)
mock_runs_response.status_code = 200
mock_runs_response.json.return_value = {
"data": [],
"meta": {"page": 1, "limit": 50, "totalItems": 0, "totalPages": 1}
}

# Mock response for delete
mock_delete_response = MagicMock(spec=httpx.Response)
mock_delete_response.status_code = 200
mock_delete_response.json.return_value = {
"message": "Dataset run deleted successfully"
}

def side_effect(request, *args, **kwargs):
url_str = str(request.url)
if "dataset-items" in url_str:
return mock_items_response
elif "/runs/" in url_str:
if request.method == "DELETE":
return mock_delete_response
return mock_run_response
elif "/runs" in url_str:
return mock_runs_response
return mock_response

mock_send.side_effect = side_effect

langfuse = Langfuse(public_key="pk-test", secret_key="sk-test", base_url="http://localhost:3000")

# 1. get_dataset
langfuse.get_dataset("my/dataset")
langfuse.get_dataset("my dataset")

# 2. get_dataset_run
langfuse.get_dataset_run(dataset_name="my/dataset", run_name="my/run")
langfuse.get_dataset_run(dataset_name="my dataset", run_name="my run")

# 3. get_dataset_runs
langfuse.get_dataset_runs(dataset_name="my/dataset")
langfuse.get_dataset_runs(dataset_name="my dataset")

# 4. delete_dataset_run
langfuse.delete_dataset_run(dataset_name="my/dataset", run_name="my/run")
langfuse.delete_dataset_run(dataset_name="my dataset", run_name="my run")

# Collect all requested URLs
requested_urls = [str(call[0][0].url) for call in mock_send.call_args_list]
for url in requested_urls:
print(url)