Skip to content

Dify plugin file upload returns tool_files IDs, but chat/workflow local_file inputs validate against upload_files, causing “Invalid upload file” #291

@tcl326

Description

@tcl326

Environment

  • Dify: 1.13.0
  • Dify Plugin SDK (dify_plugin): 0.7.2

Summary

When a plugin uploads a file via self.session.file.upload() and then invokes a chatflow app with that file as an input
using transfer_method: "local_file" + upload_file_id: , the chatflow invocation fails with HTTP 400:

{"code":"invalid_param","message":"Invalid upload file","status":400}

Expected behavior

A file uploaded through the Plugin SDK (session.file.upload()) should be usable as a chatflow app file input with
transfer_method: "local_file" by passing its returned id as upload_file_id.

Actual behavior

Chatflow invocation fails with:

  • HTTP 400
  • invalid_param
  • "Invalid upload file"

Root cause (confirmed)

Chatflow local_file parsing validates against upload_files (scoped by tenant_id), but session.file.upload()
stores the uploaded file in tool_files and returns an id that exists only in tool_files.

Evidence (SQL)

Replace <APP_ID> and <UPLOAD_ID> with your values.

  -- App tenant id
  SELECT id, tenant_id, name
  FROM apps
  WHERE id = '<APP_ID>';

  -- Fails: no row in upload_files for the upload id
  SELECT id, tenant_id, name, extension, mime_type, size, source_url
  FROM upload_files
  WHERE id = '<UPLOAD_ID>';

  -- Succeeds: row exists in tool_files for the same upload id + tenant
  SELECT id, tenant_id, name, file_key, mimetype, size, original_url
  FROM tool_files
  WHERE id = '<UPLOAD_ID>';

Minimal reproduction (plugin code)

This reproduces the failure by uploading a DOCX via session.file.upload() and then sending the returned id as a
local_file chat input.

  from dify_plugin import Endpoint
  from werkzeug import Request, Response

  class Repro(Endpoint):
      def _invoke(self, r: Request, values, settings) -> Response:
          app_id = settings["app"]["app_id"]

          content = b"PK\x03\x04..."  # any bytes; use a real docx in actual repro
          upload = self.session.file.upload(
              filename="repro.docx",
              content=content,
              mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
          )

          inputs = {
              "realfiles": [
                  {
                      "transfer_method": "local_file",
                      "upload_file_id": upload.id,
                      "type": "document",
                  }
              ]
          }

          # This invocation fails with 400: {"message":"Invalid upload file"}
          result = self.session.app.chat.invoke(
              app_id=app_id,
              query="test",
              inputs=inputs,
              response_mode="blocking",
              conversation_id=None,
          )

          return Response(response=str(result), mimetype="text/plain")

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions