|
4 | 4 | import tempfile |
5 | 5 | import typing |
6 | 6 |
|
| 7 | +import anyio |
7 | 8 | import pytest |
| 9 | +import trio |
8 | 10 |
|
9 | 11 | import httpx |
| 12 | +from httpx._multipart import FileField |
| 13 | +from httpx._types import AsyncReadableBinaryFile, is_async_readable_binary_file |
10 | 14 |
|
11 | 15 |
|
12 | 16 | def echo_request_content(request: httpx.Request) -> httpx.Response: |
@@ -467,3 +471,88 @@ def test_unicode_with_control_character(self): |
467 | 471 | files = {"upload": (filename, b"<file content>")} |
468 | 472 | request = httpx.Request("GET", "https://www.example.com", files=files) |
469 | 473 | assert expected in request.read() |
| 474 | + |
| 475 | + |
| 476 | +@pytest.mark.anyio |
| 477 | +async def test_chunked_async_file_multipart( |
| 478 | + tmp_path, anyio_backend, monkeypatch, server |
| 479 | +): |
| 480 | + total_chunks = 3 |
| 481 | + |
| 482 | + content_bytes = b"".join([b"a" * FileField.CHUNK_SIZE] * total_chunks) |
| 483 | + to_upload = tmp_path / "upload.txt" |
| 484 | + to_upload.write_bytes(content_bytes) |
| 485 | + url = server.url.copy_with(path="/echo_body") |
| 486 | + |
| 487 | + async def checks( |
| 488 | + client: httpx.AsyncClient, async_file: AsyncReadableBinaryFile |
| 489 | + ) -> None: |
| 490 | + read_called = 0 |
| 491 | + fileno_called = False |
| 492 | + original_read = async_file.read |
| 493 | + original_fileno = async_file.fileno |
| 494 | + |
| 495 | + async def mock_read(*args, **kwargs): |
| 496 | + nonlocal read_called |
| 497 | + read_called += 1 |
| 498 | + return await original_read(*args, **kwargs) |
| 499 | + |
| 500 | + def mock_fileno(*args): |
| 501 | + nonlocal fileno_called |
| 502 | + fileno_called = True |
| 503 | + return original_fileno(*args) |
| 504 | + |
| 505 | + monkeypatch.setattr(async_file, "read", mock_read) |
| 506 | + monkeypatch.setattr(async_file, "fileno", mock_fileno) |
| 507 | + response = await client.post(url=url, files={"file": async_file}) |
| 508 | + assert response.status_code == 200 |
| 509 | + boundary = response.request.headers["Content-Type"].split("boundary=")[-1] |
| 510 | + boundary_bytes = boundary.encode("ascii") |
| 511 | + pre_content = b"".join( |
| 512 | + [ |
| 513 | + b"--" + boundary_bytes + b"\r\n", |
| 514 | + b'Content-Disposition: form-data; name="file"; ' |
| 515 | + b'filename="upload.txt"\r\n', |
| 516 | + b"Content-Type: text/plain\r\n", |
| 517 | + b"\r\n", |
| 518 | + ] |
| 519 | + ) |
| 520 | + post_content = b"".join( |
| 521 | + [ |
| 522 | + b"\r\n", |
| 523 | + b"--" + boundary_bytes + b"--\r\n", |
| 524 | + ] |
| 525 | + ) |
| 526 | + assert response.content == b"".join( |
| 527 | + [ |
| 528 | + pre_content, |
| 529 | + content_bytes, |
| 530 | + post_content, |
| 531 | + ] |
| 532 | + ) |
| 533 | + assert response.request.headers["Content-Length"] == str( |
| 534 | + len(pre_content) + len(post_content) + len(content_bytes) |
| 535 | + ) |
| 536 | + assert read_called == total_chunks + 1 |
| 537 | + assert fileno_called |
| 538 | + |
| 539 | + async with ( |
| 540 | + await anyio.open_file(to_upload, mode="rb") |
| 541 | + if anyio_backend != "trio" |
| 542 | + else await trio.open_file(to_upload, mode="rb") as async_file, |
| 543 | + httpx.AsyncClient() as client, |
| 544 | + ): |
| 545 | + assert is_async_readable_binary_file(async_file) |
| 546 | + |
| 547 | + await checks(client, async_file) |
| 548 | + |
| 549 | + async with ( |
| 550 | + await anyio.open_file(to_upload, mode="rb") |
| 551 | + if anyio_backend != "trio" |
| 552 | + else await trio.open_file(to_upload, mode="rb") as async_file, |
| 553 | + ): |
| 554 | + with ( |
| 555 | + httpx.Client() as sync_client, |
| 556 | + pytest.raises(TypeError, match="AsyncReadableBinaryFile is not supported"), |
| 557 | + ): |
| 558 | + sync_client.post(url, files={"file": async_file}) |
0 commit comments