Compare commits

...

5 Commits

Author SHA1 Message Date
-LAN-
f8c6eef170 chore(Dockerfile): Upgrade dependiencies. 2024-11-08 19:34:10 +08:00
-LAN-
737b2d5238 chore: update version to 0.10.2-fix1 2024-11-08 19:28:12 +08:00
-LAN-
17ba978d1a fix(file-retrieval): improve error handling for HTTP HEAD requests
- Fallback to GET request if HEAD response status is not 200.
- Apply changes to RemoteFileInfoApi and file factory functions.
- Enhance reliability by ensuring accurate content retrieval.
2024-11-08 19:19:50 +08:00
-LAN-
fa6d2874b3 fix(http_request): send form data (#10431) 2024-11-08 17:52:56 +08:00
-LAN-
411b92893a fix(migrations): correct schema reference in service API history migration (#10452) 2024-11-08 17:52:01 +08:00
12 changed files with 133 additions and 43 deletions

View File

@@ -5,8 +5,8 @@ on:
branches: branches:
- "main" - "main"
- "deploy/dev" - "deploy/dev"
release: tags:
types: [published] - "*"
concurrency: concurrency:
group: build-push-${{ github.head_ref || github.run_id }} group: build-push-${{ github.head_ref || github.run_id }}

View File

@@ -4,7 +4,7 @@ FROM python:3.10-slim-bookworm AS base
WORKDIR /app/api WORKDIR /app/api
# Install Poetry # Install Poetry
ENV POETRY_VERSION=1.8.3 ENV POETRY_VERSION=1.8.4
# if you located in China, you can use aliyun mirror to speed up # if you located in China, you can use aliyun mirror to speed up
# RUN pip install --no-cache-dir poetry==${POETRY_VERSION} -i https://mirrors.aliyun.com/pypi/simple/ # RUN pip install --no-cache-dir poetry==${POETRY_VERSION} -i https://mirrors.aliyun.com/pypi/simple/
@@ -55,7 +55,7 @@ RUN apt-get update \
&& echo "deb http://deb.debian.org/debian testing main" > /etc/apt/sources.list \ && echo "deb http://deb.debian.org/debian testing main" > /etc/apt/sources.list \
&& apt-get update \ && apt-get update \
# For Security # For Security
&& apt-get install -y --no-install-recommends zlib1g=1:1.3.dfsg+really1.3.1-1 expat=2.6.3-1 libldap-2.5-0=2.5.18+dfsg-3+b1 perl=5.40.0-6 libsqlite3-0=3.46.1-1 \ && apt-get install -y --no-install-recommends expat=2.6.3-2 libldap-2.5-0=2.5.18+dfsg-3+b1 perl=5.40.0-6 libsqlite3-0=3.46.1-1 zlib1g=1:1.3.dfsg+really1.3.1-1+b1 \
# install a chinese font to support the use of tools like matplotlib # install a chinese font to support the use of tools like matplotlib
&& apt-get install -y fonts-noto-cjk \ && apt-get install -y fonts-noto-cjk \
&& apt-get autoremove -y \ && apt-get autoremove -y \

View File

@@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
CURRENT_VERSION: str = Field( CURRENT_VERSION: str = Field(
description="Dify version", description="Dify version",
default="0.10.2", default="0.10.2-fix1",
) )
COMMIT_SHA: str = Field( COMMIT_SHA: str = Field(

View File

@@ -89,14 +89,13 @@ class RemoteFileInfoApi(Resource):
@marshal_with(remote_file_info_fields) @marshal_with(remote_file_info_fields)
def get(self, url): def get(self, url):
decoded_url = urllib.parse.unquote(url) decoded_url = urllib.parse.unquote(url)
try: resp = ssrf_proxy.head(decoded_url)
response = ssrf_proxy.head(decoded_url) if resp.status_code != 200:
return { resp = ssrf_proxy.get(decoded_url, timeout=3)
"file_type": response.headers.get("Content-Type", "application/octet-stream"), return {
"file_length": int(response.headers.get("Content-Length", 0)), "file_type": resp.headers.get("Content-Type", "application/octet-stream"),
} "file_length": int(resp.headers.get("Content-Length", 0)),
except Exception as e: }
return {"error": str(e)}, 400
api.add_resource(FileApi, "/files/upload") api.add_resource(FileApi, "/files/upload")

View File

@@ -42,14 +42,13 @@ class RemoteFileInfoApi(WebApiResource):
@marshal_with(remote_file_info_fields) @marshal_with(remote_file_info_fields)
def get(self, url): def get(self, url):
decoded_url = urllib.parse.unquote(url) decoded_url = urllib.parse.unquote(url)
try: resp = ssrf_proxy.head(decoded_url)
response = ssrf_proxy.head(decoded_url) if resp.status_code != 200:
return { resp = ssrf_proxy.get(decoded_url, timeout=3)
"file_type": response.headers.get("Content-Type", "application/octet-stream"), return {
"file_length": int(response.headers.get("Content-Length", -1)), "file_type": resp.headers.get("Content-Type", "application/octet-stream"),
} "file_length": int(resp.headers.get("Content-Length", -1)),
except Exception as e: }
return {"error": str(e)}, 400
api.add_resource(FileApi, "/files/upload") api.add_resource(FileApi, "/files/upload")

View File

@@ -89,15 +89,6 @@ class Executor:
headers = self.variable_pool.convert_template(self.node_data.headers).text headers = self.variable_pool.convert_template(self.node_data.headers).text
self.headers = _plain_text_to_dict(headers) self.headers = _plain_text_to_dict(headers)
body = self.node_data.body
if body is None:
return
if "content-type" not in (k.lower() for k in self.headers) and body.type in BODY_TYPE_TO_CONTENT_TYPE:
self.headers["Content-Type"] = BODY_TYPE_TO_CONTENT_TYPE[body.type]
if body.type == "form-data":
self.boundary = f"----WebKitFormBoundary{_generate_random_string(16)}"
self.headers["Content-Type"] = f"multipart/form-data; boundary={self.boundary}"
def _init_body(self): def _init_body(self):
body = self.node_data.body body = self.node_data.body
if body is not None: if body is not None:
@@ -146,9 +137,8 @@ class Executor:
for k, v in files.items() for k, v in files.items()
if v.related_id is not None if v.related_id is not None
} }
self.data = form_data self.data = form_data
self.files = files self.files = files or None
def _assembling_headers(self) -> dict[str, Any]: def _assembling_headers(self) -> dict[str, Any]:
authorization = deepcopy(self.auth) authorization = deepcopy(self.auth)
@@ -209,6 +199,7 @@ class Executor:
"timeout": (self.timeout.connect, self.timeout.read, self.timeout.write), "timeout": (self.timeout.connect, self.timeout.read, self.timeout.write),
"follow_redirects": True, "follow_redirects": True,
} }
# request_args = {k: v for k, v in request_args.items() if v is not None}
response = getattr(ssrf_proxy, self.method)(**request_args) response = getattr(ssrf_proxy, self.method)(**request_args)
return response return response
@@ -236,6 +227,13 @@ class Executor:
raw += f"Host: {url_parts.netloc}\r\n" raw += f"Host: {url_parts.netloc}\r\n"
headers = self._assembling_headers() headers = self._assembling_headers()
body = self.node_data.body
boundary = f"----WebKitFormBoundary{_generate_random_string(16)}"
if body:
if "content-type" not in (k.lower() for k in self.headers) and body.type in BODY_TYPE_TO_CONTENT_TYPE:
headers["Content-Type"] = BODY_TYPE_TO_CONTENT_TYPE[body.type]
if body.type == "form-data":
headers["Content-Type"] = f"multipart/form-data; boundary={boundary}"
for k, v in headers.items(): for k, v in headers.items():
if self.auth.type == "api-key": if self.auth.type == "api-key":
authorization_header = "Authorization" authorization_header = "Authorization"
@@ -248,7 +246,6 @@ class Executor:
body = "" body = ""
if self.files: if self.files:
boundary = self.boundary
for k, v in self.files.items(): for k, v in self.files.items():
body += f"--{boundary}\r\n" body += f"--{boundary}\r\n"
body += f'Content-Disposition: form-data; name="{k}"\r\n\r\n' body += f'Content-Disposition: form-data; name="{k}"\r\n\r\n'
@@ -263,7 +260,6 @@ class Executor:
elif self.data and self.node_data.body.type == "x-www-form-urlencoded": elif self.data and self.node_data.body.type == "x-www-form-urlencoded":
body = urlencode(self.data) body = urlencode(self.data)
elif self.data and self.node_data.body.type == "form-data": elif self.data and self.node_data.body.type == "form-data":
boundary = self.boundary
for key, value in self.data.items(): for key, value in self.data.items():
body += f"--{boundary}\r\n" body += f"--{boundary}\r\n"
body += f'Content-Disposition: form-data; name="{key}"\r\n\r\n' body += f'Content-Disposition: form-data; name="{key}"\r\n\r\n'

View File

@@ -184,6 +184,8 @@ def _build_from_remote_url(
filename = url.split("/")[-1].split("?")[0] or "unknown_file" filename = url.split("/")[-1].split("?")[0] or "unknown_file"
resp = ssrf_proxy.head(url, follow_redirects=True) resp = ssrf_proxy.head(url, follow_redirects=True)
if resp.status_code != httpx.codes.OK:
resp = ssrf_proxy.get(url, follow_redirects=True, timeout=3)
if resp.status_code == httpx.codes.OK: if resp.status_code == httpx.codes.OK:
if content_disposition := resp.headers.get("Content-Disposition"): if content_disposition := resp.headers.get("Content-Disposition"):
filename = content_disposition.split("filename=")[-1].strip('"') filename = content_disposition.split("filename=")[-1].strip('"')

View File

@@ -23,7 +23,7 @@ v0_9_0_release_date= '2024-09-29 12:00:00'
def upgrade(): def upgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
sql = f"""UPDATE sql = f"""UPDATE
public.messages messages
SET SET
parent_message_id = '{UUID_NIL}' parent_message_id = '{UUID_NIL}'
WHERE WHERE
@@ -37,7 +37,7 @@ WHERE
def downgrade(): def downgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
sql = f"""UPDATE sql = f"""UPDATE
public.messages messages
SET SET
parent_message_id = NULL parent_message_id = NULL
WHERE WHERE

View File

@@ -367,3 +367,97 @@ def test_executor_with_json_body_and_nested_object_variable():
assert '"name": "John Doe"' in raw_request assert '"name": "John Doe"' in raw_request
assert '"age": 30' in raw_request assert '"age": 30' in raw_request
assert '"email": "john@example.com"' in raw_request assert '"email": "john@example.com"' in raw_request
def test_extract_selectors_from_template_with_newline():
variable_pool = VariablePool()
variable_pool.add(("node_id", "custom_query"), "line1\nline2")
node_data = HttpRequestNodeData(
title="Test JSON Body with Nested Object Variable",
method="post",
url="https://api.example.com/data",
authorization=HttpRequestNodeAuthorization(type="no-auth"),
headers="Content-Type: application/json",
params="test: {{#node_id.custom_query#}}",
body=HttpRequestNodeBody(
type="none",
data=[],
),
)
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
variable_pool=variable_pool,
)
assert executor.params == {"test": "line1\nline2"}
def test_executor_with_form_data():
# Prepare the variable pool
variable_pool = VariablePool(
system_variables={},
user_inputs={},
)
variable_pool.add(["pre_node_id", "text_field"], "Hello, World!")
variable_pool.add(["pre_node_id", "number_field"], 42)
# Prepare the node data
node_data = HttpRequestNodeData(
title="Test Form Data",
method="post",
url="https://api.example.com/upload",
authorization=HttpRequestNodeAuthorization(type="no-auth"),
headers="Content-Type: multipart/form-data",
params="",
body=HttpRequestNodeBody(
type="form-data",
data=[
BodyData(
key="text_field",
type="text",
value="{{#pre_node_id.text_field#}}",
),
BodyData(
key="number_field",
type="text",
value="{{#pre_node_id.number_field#}}",
),
],
),
)
# Initialize the Executor
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
variable_pool=variable_pool,
)
# Check the executor's data
assert executor.method == "post"
assert executor.url == "https://api.example.com/upload"
assert "Content-Type" in executor.headers
assert "multipart/form-data" in executor.headers["Content-Type"]
assert executor.params == {}
assert executor.json is None
assert executor.files is None
assert executor.content is None
# Check that the form data is correctly loaded in executor.data
assert isinstance(executor.data, dict)
assert "text_field" in executor.data
assert executor.data["text_field"] == "Hello, World!"
assert "number_field" in executor.data
assert executor.data["number_field"] == "42"
# Check the raw request (to_log method)
raw_request = executor.to_log()
assert "POST /upload HTTP/1.1" in raw_request
assert "Host: api.example.com" in raw_request
assert "Content-Type: multipart/form-data" in raw_request
assert "text_field" in raw_request
assert "Hello, World!" in raw_request
assert "number_field" in raw_request
assert "42" in raw_request

View File

@@ -2,7 +2,7 @@ version: '3'
services: services:
# API service # API service
api: api:
image: langgenius/dify-api:0.10.2 image: langgenius/dify-api:0.10.2-fix1
restart: always restart: always
environment: environment:
# Startup mode, 'api' starts the API server. # Startup mode, 'api' starts the API server.
@@ -227,7 +227,7 @@ services:
# worker service # worker service
# The Celery worker for processing the queue. # The Celery worker for processing the queue.
worker: worker:
image: langgenius/dify-api:0.10.2 image: langgenius/dify-api:0.10.2-fix1
restart: always restart: always
environment: environment:
CONSOLE_WEB_URL: '' CONSOLE_WEB_URL: ''
@@ -396,7 +396,7 @@ services:
# Frontend web application. # Frontend web application.
web: web:
image: langgenius/dify-web:0.10.2 image: langgenius/dify-web:0.10.2-fix1
restart: always restart: always
environment: environment:
# The base URL of console application api server, refers to the Console base URL of WEB service if console domain is # The base URL of console application api server, refers to the Console base URL of WEB service if console domain is

View File

@@ -242,7 +242,7 @@ x-shared-env: &shared-api-worker-env
services: services:
# API service # API service
api: api:
image: langgenius/dify-api:0.10.2 image: langgenius/dify-api:0.10.2-fix1
restart: always restart: always
environment: environment:
# Use the shared environment variables. # Use the shared environment variables.
@@ -262,7 +262,7 @@ services:
# worker service # worker service
# The Celery worker for processing the queue. # The Celery worker for processing the queue.
worker: worker:
image: langgenius/dify-api:0.10.2 image: langgenius/dify-api:0.10.2-fix1
restart: always restart: always
environment: environment:
# Use the shared environment variables. # Use the shared environment variables.
@@ -281,7 +281,7 @@ services:
# Frontend web application. # Frontend web application.
web: web:
image: langgenius/dify-web:0.10.2 image: langgenius/dify-web:0.10.2-fix1
restart: always restart: always
environment: environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-} CONSOLE_API_URL: ${CONSOLE_API_URL:-}

View File

@@ -1,6 +1,6 @@
{ {
"name": "dify-web", "name": "dify-web",
"version": "0.10.2", "version": "0.10.2-fix1",
"private": true, "private": true,
"engines": { "engines": {
"node": ">=18.17.0" "node": ">=18.17.0"