diff --git a/api/core/app/apps/advanced_chat/app_generator.py b/api/core/app/apps/advanced_chat/app_generator.py index 50a843c7e8..fdd1a776f8 100644 --- a/api/core/app/apps/advanced_chat/app_generator.py +++ b/api/core/app/apps/advanced_chat/app_generator.py @@ -452,7 +452,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): for var, val in context.items(): var.set(val) - # Save current user before entering new app context + # FIXME(-LAN-): Save current user before entering new app context from flask import g saved_user = None diff --git a/api/core/app/apps/agent_chat/app_generator.py b/api/core/app/apps/agent_chat/app_generator.py index d5ee324360..158196f24d 100644 --- a/api/core/app/apps/agent_chat/app_generator.py +++ b/api/core/app/apps/agent_chat/app_generator.py @@ -232,7 +232,7 @@ class AgentChatAppGenerator(MessageBasedAppGenerator): for var, val in context.items(): var.set(val) - # Save current user before entering new app context + # FIXME(-LAN-): Save current user before entering new app context from flask import g saved_user = None diff --git a/api/core/app/apps/workflow/app_generator.py b/api/core/app/apps/workflow/app_generator.py index 5442927413..6ea90e5a3d 100644 --- a/api/core/app/apps/workflow/app_generator.py +++ b/api/core/app/apps/workflow/app_generator.py @@ -411,7 +411,7 @@ class WorkflowAppGenerator(BaseAppGenerator): for var, val in context.items(): var.set(val) - # Save current user before entering new app context + # FIXME(-LAN-): Save current user before entering new app context from flask import g saved_user = None diff --git a/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py b/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py index a98a42f5df..6c768fd86c 100644 --- a/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py +++ b/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py @@ -455,8 +455,6 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline, MessageCycleMan agent_thought: Optional[MessageAgentThought] = ( db.session.query(MessageAgentThought).filter(MessageAgentThought.id == event.agent_thought_id).first() ) - db.session.refresh(agent_thought) - db.session.close() if agent_thought: return AgentThoughtStreamResponse( diff --git a/api/core/workflow/graph_engine/graph_engine.py b/api/core/workflow/graph_engine/graph_engine.py index 36273d8ec1..f61965e07e 100644 --- a/api/core/workflow/graph_engine/graph_engine.py +++ b/api/core/workflow/graph_engine/graph_engine.py @@ -9,7 +9,7 @@ from copy import copy, deepcopy from datetime import UTC, datetime from typing import Any, Optional, cast -from flask import Flask, current_app +from flask import Flask, current_app, has_request_context from configs import dify_config from core.app.apps.base_app_queue_manager import GenerateTaskStoppedError @@ -540,8 +540,21 @@ class GraphEngine: for var, val in context.items(): var.set(val) + # FIXME(-LAN-): Save current user before entering new app context + from flask import g + + saved_user = None + if has_request_context() and hasattr(g, "_login_user"): + saved_user = g._login_user + with flask_app.app_context(): try: + # Restore user in new app context + if saved_user is not None: + from flask import g + + g._login_user = saved_user + q.put( ParallelBranchRunStartedEvent( parallel_id=parallel_id, diff --git a/api/core/workflow/nodes/http_request/executor.py b/api/core/workflow/nodes/http_request/executor.py index 2c42f5a1be..e28ac6343b 100644 --- a/api/core/workflow/nodes/http_request/executor.py +++ b/api/core/workflow/nodes/http_request/executor.py @@ -235,6 +235,10 @@ class Executor: files[key].append(file_tuple) # convert files to list for httpx request + # If there are no actual files, we still need to force httpx to use `multipart/form-data`. + # This is achieved by inserting a harmless placeholder file that will be ignored by the server. + if not files: + self.files = [("__multipart_placeholder__", ("", b"", "application/octet-stream"))] if files: self.files = [] for key, file_tuples in files.items(): @@ -373,7 +377,10 @@ class Executor: raw += f"{k}: {v}\r\n" body_string = "" - if self.files: + # Only log actual files if present. + # '__multipart_placeholder__' is inserted to force multipart encoding but is not a real file. + # This prevents logging meaningless placeholder entries. + if self.files and not all(f[0] == "__multipart_placeholder__" for f in self.files): for key, (filename, content, mime_type) in self.files: body_string += f"--{boundary}\r\n" body_string += f'Content-Disposition: form-data; name="{key}"\r\n\r\n' diff --git a/api/core/workflow/nodes/iteration/iteration_node.py b/api/core/workflow/nodes/iteration/iteration_node.py index a061dfc354..ea0b6863c9 100644 --- a/api/core/workflow/nodes/iteration/iteration_node.py +++ b/api/core/workflow/nodes/iteration/iteration_node.py @@ -7,7 +7,7 @@ from datetime import UTC, datetime from queue import Empty, Queue from typing import TYPE_CHECKING, Any, Optional, cast -from flask import Flask, current_app +from flask import Flask, current_app, has_request_context from configs import dify_config from core.variables import ArrayVariable, IntegerVariable, NoneVariable @@ -586,7 +586,21 @@ class IterationNode(BaseNode[IterationNodeData]): """ for var, val in context.items(): var.set(val) + + # FIXME(-LAN-): Save current user before entering new app context + from flask import g + + saved_user = None + if has_request_context() and hasattr(g, "_login_user"): + saved_user = g._login_user + with flask_app.app_context(): + # Restore user in new app context + if saved_user is not None: + from flask import g + + g._login_user = saved_user + parallel_mode_run_id = uuid.uuid4().hex graph_engine_copy = graph_engine.create_copy() variable_pool_copy = graph_engine_copy.graph_runtime_state.variable_pool diff --git a/api/extensions/ext_otel.py b/api/extensions/ext_otel.py index 538a88a4fa..6dcfa7bec6 100644 --- a/api/extensions/ext_otel.py +++ b/api/extensions/ext_otel.py @@ -22,16 +22,20 @@ def on_user_loaded(_sender, user: Union["Account", "EndUser"]): from opentelemetry.trace import get_current_span if user: - current_span = get_current_span() - if isinstance(user, Account) and user.current_tenant_id: - tenant_id = user.current_tenant_id - elif isinstance(user, EndUser): - tenant_id = user.tenant_id - else: - return - if current_span: - current_span.set_attribute("service.tenant.id", tenant_id) - current_span.set_attribute("service.user.id", user.id) + try: + current_span = get_current_span() + if isinstance(user, Account) and user.current_tenant_id: + tenant_id = user.current_tenant_id + elif isinstance(user, EndUser): + tenant_id = user.tenant_id + else: + return + if current_span: + current_span.set_attribute("service.tenant.id", tenant_id) + current_span.set_attribute("service.user.id", user.id) + except Exception: + logging.exception("Error setting tenant and user attributes") + pass def init_app(app: DifyApp): @@ -54,21 +58,25 @@ def init_app(app: DifyApp): def response_hook(span: Span, status: str, response_headers: list): if span and span.is_recording(): - if status.startswith("2"): - span.set_status(StatusCode.OK) - else: - span.set_status(StatusCode.ERROR, status) - - status = status.split(" ")[0] - status_code = int(status) - status_class = f"{status_code // 100}xx" - attributes: dict[str, str | int] = {"status_code": status_code, "status_class": status_class} - request = flask.request - if request and request.url_rule: - attributes[SpanAttributes.HTTP_TARGET] = str(request.url_rule.rule) - if request and request.method: - attributes[SpanAttributes.HTTP_METHOD] = str(request.method) - _http_response_counter.add(1, attributes) + try: + if status.startswith("2"): + span.set_status(StatusCode.OK) + else: + span.set_status(StatusCode.ERROR, status) + + status = status.split(" ")[0] + status_code = int(status) + status_class = f"{status_code // 100}xx" + attributes: dict[str, str | int] = {"status_code": status_code, "status_class": status_class} + request = flask.request + if request and request.url_rule: + attributes[SpanAttributes.HTTP_TARGET] = str(request.url_rule.rule) + if request and request.method: + attributes[SpanAttributes.HTTP_METHOD] = str(request.method) + _http_response_counter.add(1, attributes) + except Exception: + logging.exception("Error setting status and attributes") + pass instrumentor = FlaskInstrumentor() if dify_config.DEBUG: @@ -99,7 +107,7 @@ def init_app(app: DifyApp): class ExceptionLoggingHandler(logging.Handler): """Custom logging handler that creates spans for logging.exception() calls""" - def emit(self, record): + def emit(self, record: logging.LogRecord): try: if record.exc_info: tracer = get_tracer_provider().get_tracer("dify.exception.logging") @@ -114,9 +122,12 @@ def init_app(app: DifyApp): }, ) as span: span.set_status(StatusCode.ERROR) - span.record_exception(record.exc_info[1]) - span.set_attribute("exception.type", record.exc_info[0].__name__) - span.set_attribute("exception.message", str(record.exc_info[1])) + if record.exc_info[1]: + span.record_exception(record.exc_info[1]) + span.set_attribute("exception.message", str(record.exc_info[1])) + if record.exc_info[0]: + span.set_attribute("exception.type", record.exc_info[0].__name__) + except Exception: pass diff --git a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py index 58b910e17b..d066fc1e33 100644 --- a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py +++ b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py @@ -246,7 +246,9 @@ def test_executor_with_form_data(): assert "multipart/form-data" in executor.headers["Content-Type"] assert executor.params == [] assert executor.json is None - assert executor.files is None + # '__multipart_placeholder__' is expected when no file inputs exist, + # to ensure the request is treated as multipart/form-data by the backend. + assert executor.files == [("__multipart_placeholder__", ("", b"", "application/octet-stream"))] assert executor.content is None # Check that the form data is correctly loaded in executor.data diff --git a/web/app/reset-password/set-password/page.tsx b/web/app/reset-password/set-password/page.tsx index dd1c4ef1f4..ee4c114a77 100644 --- a/web/app/reset-password/set-password/page.tsx +++ b/web/app/reset-password/set-password/page.tsx @@ -105,7 +105,7 @@ const ChangePasswordForm = () => {