Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions test/api/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,3 +322,151 @@ def test_streaming_exception_with_valid_status_code():
# Should use the exception's status_code
assert invoke_response.status_code == 422
assert "ExceptionWithValidStatusCode" in invoke_response.status_code_text


@pytest.mark.parametrize(
"file_data", mock_file_data, ids=[type(fd).__name__ for fd in mock_file_data]
)
def test_unstructured_ingest_error_with_status_code(file_data):
"""Test that UnstructuredIngestError with status_code is handled correctly."""
from test.assets.exception_status_code import (
function_raises_unstructured_ingest_error_with_status_code as test_fn,
)

client = TestClient(wrap_in_fastapi(func=test_fn, plugin_id="mock_plugin"))

post_body = {"file_data": file_data.model_dump()}
resp = client.post("/invoke", json=post_body)
resp_content = resp.json()
invoke_response = InvokeResponse.model_validate(resp_content)

# Should use the UnstructuredIngestError's status_code
assert invoke_response.status_code == 400
assert invoke_response.status_code_text == "Test UnstructuredIngestError with status_code"


@pytest.mark.parametrize(
"file_data", mock_file_data, ids=[type(fd).__name__ for fd in mock_file_data]
)
def test_unstructured_ingest_error_without_status_code(file_data):
"""Test that UnstructuredIngestError without status_code defaults to 500."""
from test.assets.exception_status_code import (
function_raises_unstructured_ingest_error_without_status_code as test_fn,
)

client = TestClient(wrap_in_fastapi(func=test_fn, plugin_id="mock_plugin"))

post_body = {"file_data": file_data.model_dump()}
resp = client.post("/invoke", json=post_body)
resp_content = resp.json()
invoke_response = InvokeResponse.model_validate(resp_content)

# Should default to 500 when UnstructuredIngestError has no status_code
assert invoke_response.status_code == 500
assert invoke_response.status_code_text == "Test UnstructuredIngestError without status_code"


@pytest.mark.parametrize(
"file_data", mock_file_data, ids=[type(fd).__name__ for fd in mock_file_data]
)
def test_unstructured_ingest_error_with_none_status_code(file_data):
"""Test that UnstructuredIngestError with None status_code defaults to 500."""
from test.assets.exception_status_code import (
function_raises_unstructured_ingest_error_with_none_status_code as test_fn,
)

client = TestClient(wrap_in_fastapi(func=test_fn, plugin_id="mock_plugin"))

post_body = {"file_data": file_data.model_dump()}
resp = client.post("/invoke", json=post_body)
resp_content = resp.json()
invoke_response = InvokeResponse.model_validate(resp_content)

# Should default to 500 when UnstructuredIngestError status_code is None
assert invoke_response.status_code == 500
assert invoke_response.status_code_text == "Test UnstructuredIngestError with None status_code"


@pytest.mark.parametrize(
"file_data", mock_file_data, ids=[type(fd).__name__ for fd in mock_file_data]
)
def test_async_unstructured_ingest_error(file_data):
"""Test that async functions with UnstructuredIngestError are handled correctly."""
from test.assets.exception_status_code import (
async_function_raises_unstructured_ingest_error as test_fn,
)

client = TestClient(wrap_in_fastapi(func=test_fn, plugin_id="mock_plugin"))

post_body = {"file_data": file_data.model_dump()}
resp = client.post("/invoke", json=post_body)
resp_content = resp.json()
invoke_response = InvokeResponse.model_validate(resp_content)

# Should use the UnstructuredIngestError's status_code
assert invoke_response.status_code == 503
assert invoke_response.status_code_text == "Async test UnstructuredIngestError"


def test_streaming_unstructured_ingest_error():
"""Test that async generator functions with UnstructuredIngestError are handled correctly."""
from test.assets.exception_status_code import (
async_gen_function_raises_unstructured_ingest_error as test_fn,
)

client = TestClient(wrap_in_fastapi(func=test_fn, plugin_id="mock_plugin"))

post_body = {"file_data": mock_file_data[0].model_dump()}
resp = client.post("/invoke", json=post_body)

# For streaming responses, we get NDJSON
assert resp.status_code == 200
assert resp.headers["content-type"] == "application/x-ndjson"

# Parse the streaming response - should be a single error response
lines = resp.content.decode().strip().split("\n")
assert len(lines) == 1 # Only error response since no items were yielded

# Parse the error response
import json

error_response = json.loads(lines[0])
invoke_response = InvokeResponse.model_validate(error_response)

# Should use the UnstructuredIngestError's status_code
assert invoke_response.status_code == 502
assert "Async gen test UnstructuredIngestError" in invoke_response.status_code_text


def test_streaming_unstructured_ingest_error_with_none_status_code():
"""Test that async generator functions with UnstructuredIngestError
with None status_code are handled correctly."""
from test.assets.exception_status_code import (
async_gen_function_raises_unstructured_ingest_error_with_none_status_code as test_fn,
)

client = TestClient(wrap_in_fastapi(func=test_fn, plugin_id="mock_plugin"))

post_body = {"file_data": mock_file_data[0].model_dump()}
resp = client.post("/invoke", json=post_body)

# For streaming responses, we get NDJSON
assert resp.status_code == 200
assert resp.headers["content-type"] == "application/x-ndjson"

# Parse the streaming response - should be a single error response
lines = resp.content.decode().strip().split("\n")
assert len(lines) == 1 # Only error response since no items were yielded

# Parse the error response
import json

error_response = json.loads(lines[0])
invoke_response = InvokeResponse.model_validate(error_response)

# Should default to 500 when UnstructuredIngestError status_code is None
assert invoke_response.status_code == 500
assert (
"Async gen test UnstructuredIngestError with None status_code"
in invoke_response.status_code_text
)
47 changes: 47 additions & 0 deletions test/assets/exception_status_code.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Test assets for testing exception handling with various status_code scenarios."""

from fastapi import HTTPException
from unstructured_ingest.error import UnstructuredIngestError


class ExceptionWithNoneStatusCode(Exception):
Expand Down Expand Up @@ -51,6 +52,25 @@ def function_raises_generic_exception():
raise ValueError("Generic error")


def function_raises_unstructured_ingest_error_with_status_code():
"""Function that raises UnstructuredIngestError with status_code."""
error = UnstructuredIngestError("Test UnstructuredIngestError with status_code")
error.status_code = 400
raise error


def function_raises_unstructured_ingest_error_without_status_code():
"""Function that raises UnstructuredIngestError without status_code."""
raise UnstructuredIngestError("Test UnstructuredIngestError without status_code")


def function_raises_unstructured_ingest_error_with_none_status_code():
"""Function that raises UnstructuredIngestError with None status_code."""
error = UnstructuredIngestError("Test UnstructuredIngestError with None status_code")
error.status_code = None
raise error


# Async versions for streaming response tests
async def async_function_raises_exception_with_none_status_code():
"""Async function that raises an exception with status_code=None."""
Expand All @@ -67,6 +87,13 @@ async def async_function_raises_exception_without_status_code():
raise ExceptionWithoutStatusCode("Async test exception without status_code")


async def async_function_raises_unstructured_ingest_error():
"""Async function that raises UnstructuredIngestError."""
error = UnstructuredIngestError("Async test UnstructuredIngestError")
error.status_code = 503
raise error


# Async generator versions for streaming response error tests
async def async_gen_function_raises_exception_with_none_status_code():
"""Async generator that raises an exception with status_code=None."""
Expand All @@ -90,3 +117,23 @@ async def async_gen_function_raises_exception_without_status_code():
if False: # This ensures the function is detected as a generator but never yields
yield None
raise ExceptionWithoutStatusCode("Async gen test exception without status_code")


async def async_gen_function_raises_unstructured_ingest_error():
"""Async generator that raises UnstructuredIngestError."""
# Don't yield anything, just raise the exception
if False: # This ensures the function is detected as a generator but never yields
yield None
error = UnstructuredIngestError("Async gen test UnstructuredIngestError")
error.status_code = 502
raise error


async def async_gen_function_raises_unstructured_ingest_error_with_none_status_code():
"""Async generator that raises UnstructuredIngestError with None status_code."""
# Don't yield anything, just raise the exception
if False: # This ensures the function is detected as a generator but never yields
yield None
error = UnstructuredIngestError("Async gen test UnstructuredIngestError with None status_code")
error.status_code = None
raise error
2 changes: 1 addition & 1 deletion unstructured_platform_plugins/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.0.40" # pragma: no cover
__version__ = "0.0.41" # pragma: no cover
14 changes: 14 additions & 0 deletions unstructured_platform_plugins/etl_uvicorn/api_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from pydantic import BaseModel, Field, create_model
from starlette.responses import RedirectResponse
from unstructured_ingest.data_types.file_data import BatchFileData, FileData, file_data_from_dict
from unstructured_ingest.error import UnstructuredIngestError
from uvicorn.config import LOG_LEVELS
from uvicorn.importer import import_from_string

Expand Down Expand Up @@ -223,6 +224,19 @@ async def _stream_response():
else exc.detail,
file_data=request_dict.get("file_data", None),
)
except UnstructuredIngestError as exc:
logger.error(
f"UnstructuredIngestError: {str(exc)} (status_code={exc.status_code})",
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message 'UnstructuredIngestError:' is misleading when the caught exception might be UnstructuredIngestErrorV2. Consider using a generic message like 'Unstructured ingest error:' or dynamically include the exception type name.

Suggested change
f"UnstructuredIngestError: {str(exc)} (status_code={exc.status_code})",
f"{exc.__class__.__name__}: {str(exc)} (status_code={exc.status_code})",

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

v2 removed so should be fine here

exc_info=True,
)
return InvokeResponse(
usage=usage,
message_channels=message_channels,
filedata_meta=filedata_meta_model.model_validate(filedata_meta.model_dump()),
status_code=exc.status_code or status.HTTP_500_INTERNAL_SERVER_ERROR,
status_code_text=str(exc),
file_data=request_dict.get("file_data", None),
)
except Exception as invoke_error:
logger.error(f"failed to invoke plugin: {invoke_error}", exc_info=True)
return InvokeResponse(
Expand Down