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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions