fix(files): handle empty json uploads safely (#434)

* fix(files): handle empty json uploads safely

* fix(files): normalize invalid json upload errors

* fix(files): restore file processing error import

---------

Co-authored-by: LRRuan <lrruan@users.noreply.github.com>
This commit is contained in:
LRRuan 2026-03-19 06:36:34 +08:00 committed by GitHub
parent 09a980c2fb
commit 2097c2cbcf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 84 additions and 2 deletions

View File

@ -10,7 +10,11 @@ from sqlalchemy.ext.asyncio import AsyncSession
from src import schemas
from src.config import settings
from src.exceptions import FileProcessingError, UnsupportedFileTypeError
from src.exceptions import (
FileProcessingError,
UnsupportedFileTypeError,
ValidationException,
)
from src.schemas import Message
logger = logging.getLogger(__name__)
@ -58,7 +62,19 @@ class JSONProcessor:
async def extract_text(self, content: bytes) -> str:
import json
data = json.loads(content.decode("utf-8"))
try:
decoded_content = content.decode("utf-8")
except UnicodeDecodeError as exc:
raise ValidationException("JSON uploads must be UTF-8 encoded") from exc
if not decoded_content.strip():
return ""
try:
data = json.loads(decoded_content)
except json.JSONDecodeError as exc:
raise ValidationException("Uploaded JSON is invalid") from exc
# Convert JSON to readable text format
return json.dumps(data, ensure_ascii=False)

View File

@ -134,6 +134,33 @@ async def test_create_messages_with_json_file(
assert message["session_id"] == session_name
@pytest.mark.asyncio
async def test_create_messages_with_empty_json_file(
client: TestClient,
db_session: AsyncSession,
sample_data: tuple[Workspace, Peer],
):
"""Test that empty JSON uploads do not crash and create empty content."""
test_workspace, test_peer = sample_data
test_session = await _create_test_session(db_session, test_workspace)
session_name = test_session.name
file_data = io.BytesIO(b"")
files = {"file": ("empty.json", file_data, "application/json")}
form_data = {"peer_id": test_peer.name}
url = _get_upload_url(test_workspace.name, session_name)
response = client.post(url, files=files, data=form_data)
assert response.status_code == 201
data = response.json()
assert len(data) == 1
assert data[0]["content"] == ""
assert data[0]["peer_id"] == test_peer.name
assert data[0]["session_id"] == session_name
@pytest.mark.asyncio
async def test_create_messages_with_unsupported_file_type(
client: TestClient,

39
tests/utils/test_files.py Normal file
View File

@ -0,0 +1,39 @@
import json
import pytest
from src.exceptions import ValidationException
from src.utils.files import JSONProcessor
@pytest.mark.asyncio
async def test_json_processor_returns_empty_string_for_blank_content():
processor = JSONProcessor()
assert await processor.extract_text(b"") == ""
assert await processor.extract_text(b" \n\t") == ""
@pytest.mark.asyncio
async def test_json_processor_preserves_valid_json_behavior():
processor = JSONProcessor()
result = await processor.extract_text(b'{"name": "test", "count": 1}')
assert json.loads(result) == {"name": "test", "count": 1}
@pytest.mark.asyncio
async def test_json_processor_rejects_non_utf8_content():
processor = JSONProcessor()
with pytest.raises(ValidationException, match="UTF-8"):
await processor.extract_text(b"\xff\xfe\x00{")
@pytest.mark.asyncio
async def test_json_processor_rejects_invalid_json_content():
processor = JSONProcessor()
with pytest.raises(ValidationException, match="invalid"):
await processor.extract_text(b'{"name": }')