Skip to content

Commit 26bfacc

Browse files
committed
chore: merge main into web search fix
2 parents cf37613 + 3325c46 commit 26bfacc

100 files changed

Lines changed: 4804 additions & 328 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.secrets.baseline

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,15 +1088,15 @@
10881088
"filename": "src/backend/tests/conftest.py",
10891089
"hashed_secret": "8bb6118f8fd6935ad0876a3be34a717d32708ffd",
10901090
"is_verified": false,
1091-
"line_number": 559,
1091+
"line_number": 608,
10921092
"is_secret": false
10931093
},
10941094
{
10951095
"type": "Secret Keyword",
10961096
"filename": "src/backend/tests/conftest.py",
10971097
"hashed_secret": "61fbb5a12cd7b1f1fe1624120089efc0cd299e43",
10981098
"is_verified": false,
1099-
"line_number": 769,
1099+
"line_number": 818,
11001100
"is_secret": false
11011101
}
11021102
],
@@ -1833,7 +1833,7 @@
18331833
"filename": "src/backend/tests/unit/components/files_and_knowledge/test_file_component.py",
18341834
"hashed_secret": "72cb70dbbafe97e5ea13ad88acd65d08389439b0",
18351835
"is_verified": false,
1836-
"line_number": 774,
1836+
"line_number": 817,
18371837
"is_secret": false
18381838
}
18391839
],
@@ -7248,5 +7248,5 @@
72487248
}
72497249
]
72507250
},
7251-
"generated_at": "2026-08-04T17:46:52Z"
7251+
"generated_at": "2026-08-08T15:38:52Z"
72527252
}

pyproject.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "langflow"
3-
version = "1.11.2"
3+
version = "1.11.3"
44
description = "A Python package with a built-in web application"
55
requires-python = ">=3.10,<3.15"
66
license = "MIT"
@@ -17,7 +17,7 @@ maintainers = [
1717
]
1818
# Define your main dependencies here
1919
dependencies = [
20-
"langflow-base[complete]>=0.11.2",
20+
"langflow-base[complete]>=0.11.3",
2121
# langflow-extensions:bundle-deps-start
2222
# Release coordination: langflow declares BOUNDED ranges (>=A,<B) for every
2323
# curated lfx-* package, never exact pins -- exact pins in reusable library
@@ -249,6 +249,9 @@ ignore-regex = '.*(Stati Uniti|Tense=Pres).*'
249249
# 50-min step wall. A 90s cap fires below the ~127s stall, and timeout_method="thread" dumps ALL
250250
# thread stacks (faulthandler) — naming the exact blocking call even when it is a leaked background
251251
# task. Revert to timeout=150 / timeout_method="signal" once the stall is identified and bounded.
252+
# NOTE: the thread method's timer needs the GIL, so a C-level hang that holds the GIL defeats it
253+
# (observed: release-1.11.3 py3.13 Group 5 froze 25+ min with no dump). A second, GIL-proof
254+
# faulthandler watchdog (120s, dump + exit) backstops it in src/backend/tests/conftest.py.
252255
timeout = 90
253256
timeout_method = "thread"
254257
minversion = "6.0"

src/backend/base/langflow/api/build.py

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from lfx.log.logger import logger
1414
from lfx.schema.legacy_render import project_payload_to_v1
1515
from lfx.schema.schema import InputValueRequest
16+
from lfx.utils.file_path_security import LocalFileAccessError
1617
from sqlmodel import select
1718

1819
from langflow.api.disconnect import DisconnectHandlerStreamingResponse
@@ -571,7 +572,7 @@ async def build_graph_and_get_order() -> tuple[list[str], list[str], Graph]:
571572
error_message=str(exc),
572573
)
573574

574-
if "stream or streaming set to True" in str(exc):
575+
if isinstance(exc, LocalFileAccessError) or "stream or streaming set to True" in str(exc):
575576
raise HTTPException(status_code=400, detail=str(exc)) from exc
576577
await logger.aexception("Error checking build status: " + str(exc))
577578
raise HTTPException(status_code=500, detail=str(exc)) from exc
@@ -614,22 +615,33 @@ async def create_graph(fresh_session, flow_id_str: str, flow_name: str | None) -
614615
session_id=effective_session_id,
615616
run_id=str(job_id) if job_id is not None else run_id,
616617
)
617-
if source_flow_id is not None:
618-
graph.flow_id = str(flow_id)
619-
return graph
620-
621-
if not flow_name:
622-
result = await fresh_session.exec(select(Flow.name).where(Flow.id == flow_id))
623-
flow_name = result.first()
624-
625-
return await build_graph_from_data(
626-
flow_id=flow_id_str,
627-
payload=data.model_dump(),
628-
user_id=str(current_user.id),
629-
flow_name=flow_name,
630-
session_id=effective_session_id,
631-
run_id=str(job_id) if job_id is not None else run_id,
632-
)
618+
else:
619+
if not flow_name:
620+
lookup_flow_id = source_flow_id if source_flow_id is not None else flow_id
621+
result = await fresh_session.exec(select(Flow.name).where(Flow.id == lookup_flow_id))
622+
flow_name = result.first()
623+
624+
# Sanitized public data still contains FileInput references under
625+
# the real source-flow namespace. Build under that trusted scope;
626+
# after parameter containment completes, switch the graph to the
627+
# visitor-virtual execution ID below.
628+
graph_build_flow_id = str(source_flow_id) if source_flow_id is not None else flow_id_str
629+
graph = await build_graph_from_data(
630+
flow_id=graph_build_flow_id,
631+
payload=data.model_dump(),
632+
user_id=str(current_user.id),
633+
flow_name=flow_name,
634+
session_id=effective_session_id,
635+
run_id=str(job_id) if job_id is not None else run_id,
636+
)
637+
638+
if source_flow_id is not None:
639+
# This value comes from the server-resolved public flow, never from
640+
# request data. FileInput containment and ChatInput attachment
641+
# validation use it only after flow_id becomes visitor-virtual.
642+
graph.source_flow_id = str(source_flow_id)
643+
graph.flow_id = str(flow_id)
644+
return graph
633645

634646
def sort_vertices(graph: Graph) -> list[str]:
635647
try:

src/backend/base/langflow/api/v1/chat.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from lfx.services.cache.utils import CacheMiss
1616
from lfx.utils.flow_validation import (
1717
CustomComponentValidationError,
18+
prepare_flow_build_for_user,
1819
prepare_public_flow_build,
1920
validate_flow_for_current_settings,
2021
validate_public_flow_no_code_execution,
@@ -177,6 +178,12 @@ async def retrieve_vertices_order(
177178
if not data:
178179
graph = await build_graph_from_db(flow_id=flow_id, session=session, chat_service=chat_service)
179180
else:
181+
sanitized_data = await prepare_flow_build_for_user(
182+
data.model_dump(),
183+
is_superuser=current_user.is_superuser,
184+
)
185+
if sanitized_data is not None:
186+
data = FlowDataRequest.model_validate(sanitized_data)
180187
graph = await build_and_cache_graph_from_data(
181188
flow_id=flow_id, graph_data=data.model_dump(), chat_service=chat_service
182189
)
@@ -214,6 +221,8 @@ async def retrieve_vertices_order(
214221
raise HTTPException(status_code=400, detail=str(exc)) from exc
215222
if isinstance(exc, CustomComponentValidationError):
216223
raise HTTPException(status_code=400, detail=str(exc)) from exc
224+
if isinstance(exc, RuntimeError):
225+
raise HTTPException(status_code=503, detail=str(exc)) from exc
217226
await logger.aexception("Error checking build status")
218227
raise HTTPException(status_code=500, detail=str(exc)) from exc
219228

@@ -308,7 +317,13 @@ async def build_flow(
308317

309318
try:
310319
if data:
311-
validate_flow_for_current_settings(data.model_dump())
320+
raw_data = data.model_dump()
321+
sanitized_data = await prepare_flow_build_for_user(
322+
raw_data,
323+
is_superuser=current_user.is_superuser,
324+
)
325+
if sanitized_data is not None:
326+
data = FlowDataRequest.model_validate(sanitized_data)
312327
elif flow and flow.data:
313328
validate_flow_for_current_settings(flow.data)
314329
except CustomComponentValidationError as exc:

src/backend/base/langflow/api/v1/flows.py

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88
from uuid import UUID
99

1010
import orjson
11-
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
11+
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile, status
1212
from fastapi.encoders import jsonable_encoder
1313
from fastapi_pagination import Page, Params
1414
from fastapi_pagination.ext.sqlmodel import apaginate
15+
from lfx.log.logger import logger
1516
from lfx.services.cache.utils import CACHE_MISS
1617
from pydantic import ValidationError
1718
from sqlmodel import and_, col, select
@@ -56,6 +57,10 @@
5657
from langflow.services.authorization.fetch import deny_to_404
5758
from langflow.services.authorization.utils import _resolve_authz_domain
5859
from langflow.services.cache.service import ThreadingInMemoryCache
60+
from langflow.services.database.lock_retry import (
61+
is_database_lock_error,
62+
run_with_lock_retry,
63+
)
5964
from langflow.services.database.models.deployment.exceptions import (
6065
araise_if_deployment_guard_error_or_skip,
6166
)
@@ -74,6 +79,7 @@
7479
# and FlowVersionError from the flow_version modules.
7580
from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME
7681
from langflow.services.database.models.folder.model import Folder
82+
from langflow.services.database.models.user.model import UserRead
7783
from langflow.services.deps import get_settings_service, get_storage_service
7884
from langflow.services.storage.service import StorageService
7985
from langflow.utils.compression import compress_response
@@ -103,6 +109,9 @@ def _handle_unique_constraint_error(exc: Exception, *, status_code: int = 400) -
103109
# build router
104110
router = APIRouter(prefix="/flows", tags=["Flows"])
105111

112+
FLOW_UPDATE_FAILED = "Could not update the flow."
113+
FLOW_UPDATE_BUSY = "The database is busy. Please retry the request."
114+
106115

107116
@router.post("/", response_model=FlowRead, status_code=201)
108117
async def create_flow(
@@ -328,6 +337,7 @@ async def update_flow(
328337
storage_service: Annotated[StorageService, Depends(get_storage_service)],
329338
):
330339
"""Update a flow."""
340+
actor = UserRead.model_validate(current_user, from_attributes=True)
331341
try:
332342
# Destination check: if the payload moves the flow into a new
333343
# workspace/folder, the caller must also be authorized to write at the
@@ -339,7 +349,7 @@ async def update_flow(
339349
if target_workspace_id != db_flow.workspace_id or target_folder_id != db_flow.folder_id:
340350
try:
341351
await ensure_flow_permission(
342-
current_user,
352+
actor,
343353
FlowAction.WRITE,
344354
flow_id=flow_id,
345355
flow_user_id=db_flow.user_id,
@@ -357,7 +367,7 @@ async def update_flow(
357367

358368
async def operation() -> FlowRead:
359369
# Re-load inside each attempt so retry after nested rollback never uses an expired ORM instance.
360-
db_flow_for_attempt = await _read_flow(session=session, flow_id=flow_id, user_id=current_user.id)
370+
db_flow_for_attempt = await _read_flow(session=session, flow_id=flow_id, user_id=actor.id)
361371
if not db_flow_for_attempt:
362372
raise HTTPException(status_code=404, detail="Flow not found")
363373
# TOCTOU: a concurrent PATCH could have moved this flow to a
@@ -367,7 +377,7 @@ async def operation() -> FlowRead:
367377
# stale check across a race.
368378
try:
369379
await ensure_flow_permission(
370-
current_user,
380+
actor,
371381
FlowAction.WRITE,
372382
flow_id=flow_id,
373383
flow_user_id=db_flow_for_attempt.user_id,
@@ -386,7 +396,7 @@ async def operation() -> FlowRead:
386396
):
387397
try:
388398
await ensure_flow_permission(
389-
current_user,
399+
actor,
390400
FlowAction.WRITE,
391401
flow_id=flow_id,
392402
flow_user_id=db_flow_for_attempt.user_id,
@@ -399,26 +409,47 @@ async def operation() -> FlowRead:
399409
session=session,
400410
db_flow=db_flow_for_attempt,
401411
flow=flow,
402-
user_id=current_user.id,
412+
user_id=actor.id,
403413
storage_service=storage_service,
404414
)
405415

406-
if folder_id_will_change:
407-
return await retry_flow_operation_on_deployment_guard(
408-
db=session,
409-
user_id=current_user.id,
410-
flow_ids=[flow_id],
411-
operation=operation,
412-
)
413-
return await operation()
416+
async def update_attempt(_attempt: int) -> FlowRead:
417+
if folder_id_will_change:
418+
return await retry_flow_operation_on_deployment_guard(
419+
db=session,
420+
user_id=actor.id,
421+
flow_ids=[flow_id],
422+
operation=operation,
423+
)
424+
return await operation()
425+
426+
return await run_with_lock_retry(
427+
update_attempt,
428+
session=session,
429+
description=f"update_flow {flow_id}",
430+
)
414431
except HTTPException:
415432
raise
416433
except Exception as e:
417434
await araise_if_deployment_guard_error_or_skip(
418435
e,
419436
log_message=f"op=update_flow flow_id={flow_id}",
420437
)
421-
raise _handle_unique_constraint_error(e) from e
438+
if is_database_lock_error(e):
439+
await logger.awarning("op=update_flow flow_id=%s exhausted lock retries", flow_id)
440+
raise HTTPException(
441+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
442+
detail=FLOW_UPDATE_BUSY,
443+
headers={"Retry-After": "1"},
444+
) from e
445+
handled_error = _handle_unique_constraint_error(e)
446+
if handled_error.status_code != status.HTTP_500_INTERNAL_SERVER_ERROR:
447+
raise handled_error from e
448+
await logger.aerror("op=update_flow flow_id=%s failed with %s", flow_id, type(e).__name__)
449+
raise HTTPException(
450+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
451+
detail=FLOW_UPDATE_FAILED,
452+
) from e
422453

423454

424455
@router.put("/{flow_id}", response_model=FlowRead)

src/backend/base/langflow/api/v1/mcp_projects.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
from langflow.services.database.models.user.crud import get_user_by_username
8484
from langflow.services.database.models.user.model import User
8585
from langflow.services.deps import get_service
86+
from langflow.services.rate_limit.service import get_last_forwarded_for_hop
8687

8788
# Constants
8889
ALL_INTERFACES_HOST = "0.0.0.0" # noqa: S104
@@ -792,6 +793,9 @@ def get_client_ip(request: Request) -> str:
792793
(``rate_limit_trust_proxy``) do we consult ``X-Forwarded-For``, and then we
793794
take the rightmost entry — the last hop added by the trusted proxy, which a
794795
client cannot forge — mirroring ``langflow.services.rate_limit.service.get_client_ip``.
796+
Every occurrence of the header is joined first, so a proxy that appends its
797+
own line rather than extending the client's cannot leave the attacker's line
798+
as the one we read.
795799
796800
Args:
797801
request: FastAPI Request object
@@ -802,11 +806,9 @@ def get_client_ip(request: Request) -> str:
802806
# Only consult X-Forwarded-For when an operator has explicitly declared a
803807
# trusted proxy; otherwise the header is attacker-controlled.
804808
if get_settings_service().settings.rate_limit_trust_proxy:
805-
forwarded_for = request.headers.get("X-Forwarded-For")
806-
if forwarded_for:
807-
# Rightmost entry = last hop added by the trusted proxy (unspoofable);
808-
# the leftmost entry is client-supplied and must never be trusted.
809-
return forwarded_for.split(",")[-1].strip()
809+
last_hop = get_last_forwarded_for_hop(request)
810+
if last_hop:
811+
return last_hop
810812

811813
# Default: trust only the real TCP peer.
812814
if request.client:

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,8 @@ async def authorize_flow_action(
221221
) from err
222222

223223

224-
def _apply_execution_gates(parsed, flow, current_user: UserRead) -> None:
225-
"""The langflow request gates that run before a flow executes."""
224+
def _apply_execution_gates(parsed, flow, current_user: UserRead):
225+
"""Run request gates and return any server-sanitized execution payload."""
226226
_reject_unsupported_sync_fields(parsed)
227227
_reject_sync_only_fields(parsed)
228228
try:
@@ -234,7 +234,7 @@ def _apply_execution_gates(parsed, flow, current_user: UserRead) -> None:
234234
if exc.status_code == status.HTTP_404_NOT_FOUND:
235235
raise _flow_not_found_http_exception(str(parsed.flow_id)) from exc
236236
raise
237-
_validate_flow_data_for_execution(parsed, flow)
237+
return _validate_flow_data_for_execution(parsed, flow, current_user)
238238

239239

240240
async def run_sync_with_mapping(
@@ -246,7 +246,7 @@ async def run_sync_with_mapping(
246246
background_tasks: BackgroundTasks,
247247
) -> WorkflowExecutionResponse:
248248
"""Inline sync run with the langflow timeout/validation error mapping."""
249-
_apply_execution_gates(parsed, flow, current_user)
249+
parsed = _apply_execution_gates(parsed, flow, current_user)
250250
job_id = uuid4()
251251
try:
252252
return await execute_sync_workflow_with_timeout(
@@ -321,7 +321,7 @@ def build_stream_response(
321321
side-channel, and vertex-build persistence all survive. Validation gates run
322322
before the response is constructed so a bad request fails before streaming.
323323
"""
324-
_apply_execution_gates(parsed, flow, current_user)
324+
parsed = _apply_execution_gates(parsed, flow, current_user)
325325
adapter = get_stream_adapter(
326326
stream_protocol,
327327
StreamAdapterContext(
@@ -346,7 +346,7 @@ async def submit_background_with_mapping(
346346
stream_protocol: str,
347347
) -> WorkflowJobResponse:
348348
"""Queue a durable background run with the langflow service error mapping."""
349-
_apply_execution_gates(parsed, flow, current_user)
349+
parsed = _apply_execution_gates(parsed, flow, current_user)
350350
try:
351351
return await execute_workflow_background(
352352
parsed=parsed,

0 commit comments

Comments
 (0)