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
17 changes: 12 additions & 5 deletions packages/sdk/src/pipefy_sdk/services/attachment_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,18 @@ async def upload_attachment(
)
upload_url = upload_url.strip()

put_result = await self._s3_uploader.put(
url=upload_url,
bytes_=file.bytes,
content_type=effective_type,
)
try:
put_result = await self._s3_uploader.put(
url=upload_url,
bytes_=file.bytes,
content_type=effective_type,
)
except Exception as exc:
# Transport errors and the uploader's host-allowlist rejection are
# s3_upload-stage failures; tag them so the step contract holds.
raise AttachmentUploadError(
f"S3 upload failed: {exc}", step="s3_upload"
) from exc
status = put_result.get("status_code", 0)
if not isinstance(status, int) or status >= 400:
body_snippet = put_result.get("body_snippet")
Expand Down
21 changes: 21 additions & 0 deletions packages/sdk/tests/services/test_attachment_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,27 @@ async def test_upload_attachment_s3_http_error_carries_snippet_and_status(tmp_pa
assert ctx.value.status_code == 403


@pytest.mark.unit
@pytest.mark.asyncio
async def test_upload_attachment_s3_put_exception_tagged_s3_upload(tmp_path):
"""A raising PUT (transport error, allowlist rejection) carries the step tag."""
service, _ = _make_service()
service._s3_uploader.put = AsyncMock(
side_effect=ConnectionError("connection reset by peer")
)
attachment = _build_attachment(tmp_path, name="a.bin")

with pytest.raises(AttachmentUploadError) as ctx:
await service.upload_attachment(
attachment,
organization_id="org-1",
target=CardTarget(card_id="c1", field_id="f"),
)
assert ctx.value.step == "s3_upload"
assert "connection reset by peer" in str(ctx.value)
assert isinstance(ctx.value.__cause__, ConnectionError)


@pytest.mark.unit
@pytest.mark.asyncio
async def test_upload_attachment_extract_storage_path_failure_maps_to_s3_step(tmp_path):
Expand Down