Skip to content

Commit 48c5d42

Browse files
Cristhianzlvjgit96autofix-ci[bot]
authored
fix: Add ephemeral file upload and credential env fallback (#12333)
Co-authored-by: vjgit96 <vijay.katuri@ibm.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> fix: replace grep -oP with sed for Node.js version extraction in Docker images (#12330) fix as PR #12309 on release-1.9.0.
1 parent 63ad0ec commit 48c5d42

11 files changed

Lines changed: 934 additions & 389 deletions

docker/build_and_push.Dockerfile

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,11 @@ RUN ARCH=$(dpkg --print-architecture) \
8888
elif [ "$ARCH" = "arm64" ]; then NODE_ARCH="arm64"; \
8989
else NODE_ARCH="$ARCH"; fi \
9090
&& NODE_VERSION=$(curl -fsSL https://nodejs.org/dist/latest-v22.x/ \
91-
| grep -oP "node-v\K[0-9]+\.[0-9]+\.[0-9]+(?=-linux-${NODE_ARCH}\.tar\.xz)" \
91+
| sed -nE "s/.*node-v([0-9]+\.[0-9]+\.[0-9]+)-linux-${NODE_ARCH}\.tar\.xz.*/\1/p" \
9292
| head -1) \
93+
&& if [ -z "$NODE_VERSION" ]; then echo "ERROR: Could not determine Node.js version" && exit 1; fi \
9394
&& curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz" \
94-
| tar -xJ -C /usr/local --strip-components=1 \
95-
&& npm install -g npm@latest \
96-
&& npm cache clean --force
95+
| tar -xJ -C /usr/local --strip-components=1
9796
RUN useradd user -u 1000 -g 0 --no-create-home --home-dir /app/data
9897

9998
COPY --from=builder --chown=1000 /app/.venv /app/.venv

docker/build_and_push_backend.Dockerfile

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,11 @@ RUN ARCH=$(dpkg --print-architecture) \
6363
elif [ "$ARCH" = "arm64" ]; then NODE_ARCH="arm64"; \
6464
else NODE_ARCH="$ARCH"; fi \
6565
&& NODE_VERSION=$(curl -fsSL https://nodejs.org/dist/latest-v22.x/ \
66-
| grep -oP "node-v\K[0-9]+\.[0-9]+\.[0-9]+(?=-linux-${NODE_ARCH}\.tar\.xz)" \
66+
| sed -nE "s/.*node-v([0-9]+\.[0-9]+\.[0-9]+)-linux-${NODE_ARCH}\.tar\.xz.*/\1/p" \
6767
| head -1) \
68+
&& if [ -z "$NODE_VERSION" ]; then echo "ERROR: Could not determine Node.js version" && exit 1; fi \
6869
&& curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz" \
69-
| tar -xJ -C /usr/local --strip-components=1 \
70-
&& npm install -g npm@latest \
71-
&& npm cache clean --force
70+
| tar -xJ -C /usr/local --strip-components=1
7271

7372
# Create non-root user
7473
RUN useradd --uid 1000 --gid 0 --no-create-home --home-dir /app/data user

docker/build_and_push_base.Dockerfile

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,11 @@ RUN ARCH=$(dpkg --print-architecture) \
8989
elif [ "$ARCH" = "arm64" ]; then NODE_ARCH="arm64"; \
9090
else NODE_ARCH="$ARCH"; fi \
9191
&& NODE_VERSION=$(curl -fsSL https://nodejs.org/dist/latest-v22.x/ \
92-
| grep -oP "node-v\K[0-9]+\.[0-9]+\.[0-9]+(?=-linux-${NODE_ARCH}\.tar\.xz)" \
92+
| sed -nE "s/.*node-v([0-9]+\.[0-9]+\.[0-9]+)-linux-${NODE_ARCH}\.tar\.xz.*/\1/p" \
9393
| head -1) \
94+
&& if [ -z "$NODE_VERSION" ]; then echo "ERROR: Could not determine Node.js version" && exit 1; fi \
9495
&& curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz" \
95-
| tar -xJ -C /usr/local --strip-components=1 \
96-
&& npm install -g npm@latest \
97-
&& npm cache clean --force
96+
| tar -xJ -C /usr/local --strip-components=1
9897
RUN useradd user -u 1000 -g 0 --no-create-home --home-dir /app/data
9998

10099
COPY --from=builder --chown=1000 /app/.venv /app/.venv

docker/build_and_push_ep.Dockerfile

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,11 @@ RUN ARCH=$(dpkg --print-architecture) \
8484
elif [ "$ARCH" = "arm64" ]; then NODE_ARCH="arm64"; \
8585
else NODE_ARCH="$ARCH"; fi \
8686
&& NODE_VERSION=$(curl -fsSL https://nodejs.org/dist/latest-v22.x/ \
87-
| grep -oP "node-v\K[0-9]+\.[0-9]+\.[0-9]+(?=-linux-${NODE_ARCH}\.tar\.xz)" \
87+
| sed -nE "s/.*node-v([0-9]+\.[0-9]+\.[0-9]+)-linux-${NODE_ARCH}\.tar\.xz.*/\1/p" \
8888
| head -1) \
89+
&& if [ -z "$NODE_VERSION" ]; then echo "ERROR: Could not determine Node.js version" && exit 1; fi \
8990
&& curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz" \
90-
| tar -xJ -C /usr/local --strip-components=1 \
91-
&& npm install -g npm@latest \
92-
&& npm cache clean --force
91+
| tar -xJ -C /usr/local --strip-components=1
9392
RUN useradd user -u 1000 -g 0 --no-create-home --home-dir /app/data
9493

9594
COPY --from=builder --chown=1000 /app/.venv /app/.venv

docker/build_and_push_with_extras.Dockerfile

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,11 @@ RUN ARCH=$(dpkg --print-architecture) \
8585
elif [ "$ARCH" = "arm64" ]; then NODE_ARCH="arm64"; \
8686
else NODE_ARCH="$ARCH"; fi \
8787
&& NODE_VERSION=$(curl -fsSL https://nodejs.org/dist/latest-v22.x/ \
88-
| grep -oP "node-v\K[0-9]+\.[0-9]+\.[0-9]+(?=-linux-${NODE_ARCH}\.tar\.xz)" \
88+
| sed -nE "s/.*node-v([0-9]+\.[0-9]+\.[0-9]+)-linux-${NODE_ARCH}\.tar\.xz.*/\1/p" \
8989
| head -1) \
90+
&& if [ -z "$NODE_VERSION" ]; then echo "ERROR: Could not determine Node.js version" && exit 1; fi \
9091
&& curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz" \
91-
| tar -xJ -C /usr/local --strip-components=1 \
92-
&& npm install -g npm@latest \
93-
&& npm cache clean --force
92+
| tar -xJ -C /usr/local --strip-components=1
9493
RUN useradd user -u 1000 -g 0 --no-create-home --home-dir /app/data
9594

9695
COPY --from=builder --chown=1000 /app/.venv /app/.venv

src/backend/base/langflow/api/v2/files.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ async def upload_user_file(
137137
settings_service: Annotated[SettingsService, Depends(get_settings_service)],
138138
*,
139139
append: bool = False,
140+
ephemeral: bool = False,
140141
) -> UploadFileResponse:
141142
"""Upload a file for the current user and track it in the database."""
142143
# Get the max allowed file size from settings (in MB)
@@ -232,6 +233,12 @@ async def upload_user_file(
232233
# General error saving file or getting file size
233234
raise HTTPException(status_code=500, detail=f"Error accessing file: {e}") from e
234235

236+
if ephemeral:
237+
# Ephemeral uploads: file is saved to storage (servable for chat history)
238+
# but no UserFile record is created (won't appear in "My Files")
239+
file_path = f"{current_user.id}/{stored_file_name}"
240+
return UploadFileResponse(id=file_id, name=root_filename, path=file_path, size=file_size)
241+
235242
if append and existing_file:
236243
existing_file.size = file_size
237244
session.add(existing_file)

src/backend/tests/unit/api/v2/test_files.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,77 @@ async def test_upload_file(files_client, files_created_api_key):
149149
assert "id" in response_json
150150

151151

152+
async def test_should_not_persist_in_my_files_when_upload_is_ephemeral(files_client, files_created_api_key):
153+
"""Ephemeral uploads save the file to storage but do NOT create a UserFile DB record.
154+
155+
This is the expected behavior for chat playground uploads in Desktop,
156+
where the file must be servable (for chat history) but should not
157+
appear in the user's 'My Files' list.
158+
"""
159+
headers = {"x-api-key": files_created_api_key.api_key}
160+
161+
# Upload with ephemeral=true
162+
response = await files_client.post(
163+
"api/v2/files",
164+
files={"file": ("playground_image.png", b"fake image content")},
165+
params={"ephemeral": "true"},
166+
headers=headers,
167+
)
168+
assert response.status_code == 201, f"Expected 201, got {response.status_code}: {response.text}"
169+
170+
upload_response = response.json()
171+
assert "path" in upload_response
172+
173+
# The file must NOT appear in the user's file list
174+
list_response = await files_client.get("api/v2/files", headers=headers)
175+
assert list_response.status_code == 200
176+
file_names = [f["name"] for f in list_response.json()]
177+
assert "playground_image" not in file_names, (
178+
f"Ephemeral file should not appear in My Files, but found: {file_names}"
179+
)
180+
181+
# The file is saved in storage and the response includes a valid path
182+
file_path = upload_response["path"]
183+
assert file_path, "Ephemeral upload should return a non-empty path"
184+
# Path format: {user_id}/{stored_file_name}
185+
parts = file_path.split("/")
186+
assert len(parts) == 2, f"Expected path format 'user_id/filename', got: {file_path}"
187+
188+
189+
async def test_should_return_path_with_forward_slashes_when_uploading_file(files_client, files_created_api_key):
190+
"""Upload response path must use forward slashes on all platforms.
191+
192+
On Windows, pathlib.Path serializes with backslashes, but the GET list
193+
endpoint returns the raw DB string with forward slashes. If the POST
194+
response uses backslashes, the frontend cannot match them with
195+
selectedFiles.includes(file.path), leaving checkboxes unchecked.
196+
"""
197+
headers = {"x-api-key": files_created_api_key.api_key}
198+
199+
response = await files_client.post(
200+
"api/v2/files",
201+
files={"file": ("test_path.txt", b"path test content")},
202+
headers=headers,
203+
)
204+
assert response.status_code == 201
205+
206+
upload_path = response.json()["path"]
207+
assert "\\" not in upload_path, (
208+
f"Upload response path contains backslashes: '{upload_path}'. "
209+
"Path must use forward slashes on all platforms for frontend compatibility."
210+
)
211+
212+
# Verify the upload path matches what GET /files returns
213+
list_response = await files_client.get("api/v2/files", headers=headers)
214+
assert list_response.status_code == 200
215+
216+
listed_paths = [f["path"] for f in list_response.json()]
217+
assert upload_path in listed_paths, (
218+
f"Upload path '{upload_path}' not found in listed paths {listed_paths}. "
219+
"POST and GET must return identical path strings."
220+
)
221+
222+
152223
async def test_download_file(files_client, files_created_api_key):
153224
headers = {"x-api-key": files_created_api_key.api_key}
154225

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""Tests for credential resolution in the unified models system.
2+
3+
Bug: Desktop Global Variable OPENAI_API_KEY not injected at runtime.
4+
When api_key parameter is None (Agent component default), get_api_key_for_provider
5+
only attempts DB lookup but has no os.getenv() fallback and no error handling
6+
for ValueError from get_variable(). This causes failures in Desktop where the
7+
env var is not set and the DB lookup is the only path.
8+
"""
9+
10+
from unittest.mock import patch
11+
from uuid import uuid4
12+
13+
14+
class TestGetApiKeyForProviderDbFallback:
15+
"""Tests for get_api_key_for_provider when api_key param is None (second path)."""
16+
17+
@patch("lfx.base.models.unified_models.get_model_provider_variable_mapping")
18+
@patch("lfx.base.models.unified_models.run_until_complete")
19+
def test_should_fallback_to_env_when_db_lookup_raises_value_error(self, mock_run, mock_mapping, monkeypatch):
20+
"""When variable_service.get_variable raises ValueError (variable not found in DB).
21+
22+
get_api_key_for_provider should fall back to os.getenv() instead of returning None.
23+
"""
24+
from lfx.base.models.unified_models import get_api_key_for_provider
25+
26+
user_id = str(uuid4())
27+
mock_mapping.return_value = {"OpenAI": "OPENAI_API_KEY"}
28+
mock_run.side_effect = ValueError("OPENAI_API_KEY variable not found.")
29+
30+
monkeypatch.setenv("OPENAI_API_KEY", "sk-test-env-key")
31+
32+
result = get_api_key_for_provider(user_id, "OpenAI", None)
33+
34+
assert result == "sk-test-env-key"
35+
36+
@patch("lfx.base.models.unified_models.get_model_provider_variable_mapping")
37+
@patch("lfx.base.models.unified_models.run_until_complete")
38+
def test_should_fallback_to_env_when_db_lookup_returns_empty_string(self, mock_run, mock_mapping, monkeypatch):
39+
"""When decryption fails, get_variable returns empty string.
40+
41+
get_api_key_for_provider should fall back to os.getenv().
42+
"""
43+
from lfx.base.models.unified_models import get_api_key_for_provider
44+
45+
user_id = str(uuid4())
46+
mock_mapping.return_value = {"OpenAI": "OPENAI_API_KEY"}
47+
mock_run.return_value = ""
48+
49+
monkeypatch.setenv("OPENAI_API_KEY", "sk-test-env-key")
50+
51+
result = get_api_key_for_provider(user_id, "OpenAI", None)
52+
53+
assert result == "sk-test-env-key"
54+
55+
@patch("lfx.base.models.unified_models.get_model_provider_variable_mapping")
56+
@patch("lfx.base.models.unified_models.run_until_complete")
57+
def test_should_fallback_to_env_when_variable_service_is_none(self, mock_run, mock_mapping, monkeypatch):
58+
"""When variable_service is None (service not available in thread context).
59+
60+
get_api_key_for_provider should fall back to os.getenv().
61+
"""
62+
from lfx.base.models.unified_models import get_api_key_for_provider
63+
64+
user_id = str(uuid4())
65+
mock_mapping.return_value = {"OpenAI": "OPENAI_API_KEY"}
66+
mock_run.return_value = None
67+
68+
monkeypatch.setenv("OPENAI_API_KEY", "sk-test-env-key")
69+
70+
result = get_api_key_for_provider(user_id, "OpenAI", None)
71+
72+
assert result == "sk-test-env-key"
73+
74+
@patch("lfx.base.models.unified_models.get_model_provider_variable_mapping")
75+
@patch("lfx.base.models.unified_models.run_until_complete")
76+
def test_should_return_none_when_both_db_and_env_unavailable(self, mock_run, mock_mapping, monkeypatch):
77+
"""When both DB lookup and env var are unavailable, should return None."""
78+
from lfx.base.models.unified_models import get_api_key_for_provider
79+
80+
user_id = str(uuid4())
81+
mock_mapping.return_value = {"OpenAI": "OPENAI_API_KEY"}
82+
mock_run.return_value = None
83+
84+
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
85+
86+
result = get_api_key_for_provider(user_id, "OpenAI", None)
87+
88+
assert result is None
89+
90+
@patch("lfx.base.models.unified_models.get_model_provider_variable_mapping")
91+
@patch("lfx.base.models.unified_models.run_until_complete")
92+
def test_should_return_db_value_when_db_lookup_succeeds(self, mock_run, mock_mapping):
93+
"""When DB lookup succeeds, should return the DB value (no env fallback needed)."""
94+
from lfx.base.models.unified_models import get_api_key_for_provider
95+
96+
user_id = str(uuid4())
97+
mock_mapping.return_value = {"OpenAI": "OPENAI_API_KEY"}
98+
mock_run.return_value = "sk-from-database"
99+
100+
result = get_api_key_for_provider(user_id, "OpenAI", None)
101+
102+
assert result == "sk-from-database"

src/lfx/src/lfx/_assets/component_index.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118191,6 +118191,6 @@
118191118191
"num_components": 359,
118192118192
"num_modules": 97
118193118193
},
118194-
"sha256": "c2c714d9bec52ae89ed53341db3be1653da036cb8259013f204df4707bdbf757",
118195-
"version": "0.3.1"
118194+
"sha256": "e1d9ffffbffc29b15303caf157393d6cf1489763c218a0a8f5187eb395085593",
118195+
"version": "0.3.2"
118196118196
}

0 commit comments

Comments
 (0)