Skip to content

[FEEDS-1206]fix file uploads#223

Merged
itsmeadi merged 4 commits intomainfrom
file_upload_fix
Apr 1, 2026
Merged

[FEEDS-1206]fix file uploads#223
itsmeadi merged 4 commits intomainfrom
file_upload_fix

Conversation

@itsmeadi
Copy link
Copy Markdown
Contributor

@itsmeadi itsmeadi commented Mar 18, 2026

Summary by CodeRabbit

  • New Features

    • Channel file and image uploads with optional user context, automatic filename/MIME detection, and optional image size specifications.
    • General-purpose file and image upload endpoints for non-channel uploads.
    • Uploads available in both synchronous and asynchronous clients; async image upload preserves backward-compatible parameter handling and enforces runtime argument validation.
  • Tests

    • Added/updated tests exercising upload and delete flows using temporary file paths and integration-style file/image upload checks.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 18, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f0024858-d1cc-492a-a1e9-f8715d494d97

📥 Commits

Reviewing files that changed from the base of the PR and between 6de7e2c and c927b2c.

📒 Files selected for processing (2)
  • getstream/chat/async_client.py
  • getstream/chat/client.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • getstream/chat/async_client.py

📝 Walkthrough

Walkthrough

Adds multipart/form-data upload support: new _upload_multipart helpers in sync/async base clients; public upload APIs on CommonClient and ChatClient (sync + async) for files/images; and tests updated to use temporary file paths and exercise the new upload flows.

Changes

Cohort / File(s) Summary
Base Upload Infrastructure
getstream/base.py
Added _read_file_bytes and _upload_multipart on BaseClient and AsyncBaseClient: read file bytes (sync or via asyncio.to_thread), infer MIME type, build files and optional data from form_fields, and POST via existing request methods.
Chat Client (sync + async)
getstream/chat/client.py, getstream/chat/async_client.py
Added upload_channel_file and upload_channel_image methods (sync + async). Assemble form_fields (optional user, upload_sizes), handle legacy type arg normalization, validate args, and call _upload_multipart for channel file/image endpoints.
Common Client (sync + async)
getstream/common/client.py, getstream/common/async_client.py
Added upload_file and upload_image methods (sync + async). Assemble form_fields (optional user, upload_sizes) and delegate to _upload_multipart for /api/v2/uploads/{file,image} endpoints.
Tests
tests/test_chat_channel.py, tests/test_chat_misc.py
Refactored tests to use tmp_path temporary files for uploads, removed multipart skipping, added integration-style upload/delete tests, updated test signatures and imports (including OnlyUserID).

Sequence Diagram

sequenceDiagram
    participant Client as Client\n(ChatClient / CommonClient)
    participant Base as BaseClient\n(BaseClient / AsyncBaseClient)
    participant FS as File System
    participant HTTP as HTTP Layer
    participant Server as Server

    Client->>Base: _upload_multipart(path, data_type, file_path, form_fields)
    Base->>FS: Read file bytes (sync or via asyncio.to_thread)
    FS-->>Base: file bytes
    Base->>Base: Infer MIME type, build multipart payload (files, data)
    Base->>HTTP: POST multipart/form-data to path
    HTTP->>Server: deliver request
    Server-->>HTTP: response
    HTTP-->>Base: response data
    Base-->>Client: StreamResponse[T]
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I nibbled bytes from a path so neat,
Sewed fields and files into one tidy sheet,
Hop-ping from client down to the server door,
Multipart carrots scattered—uploads galore! 🥕✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.31% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title '[FEEDS-1206]fix file uploads' is partially related to the changeset—it mentions file uploads, which is a key aspect, but inadequately captures the main implementation: adding multipart/form-data upload support infrastructure and multiple public upload methods across CommonClient and ChatClient. Consider revising to more precisely describe the primary change, e.g., 'Add multipart upload support with file and image upload methods' or '[FEEDS-1206] Implement multipart file and image uploads'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch file_upload_fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@getstream/chat/async_client.py`:
- Around line 31-47: The upload_channel_file method declares file as
Optional[str] but passes it to _upload_multipart which requires a non-optional
str; change the signature of upload_channel_file (the function named
upload_channel_file) to make file: str (drop Optional) and update any callers if
necessary so a file is always provided, then call _upload_multipart with that
non-optional file value (referencing _upload_multipart and the path_params
usage) to ensure type consistency.
- Around line 50-71: The parameter name "type" in upload_channel_image shadows
the built-in and matches the earlier "Same type issue" — rename the parameter to
channel_type (and update any references) so the signature becomes
upload_channel_image(self, channel_type: str, id: str, ...), then pass it into
path_params as {"type": channel_type, "id": id} when calling _upload_multipart;
update any internal references or callers of upload_channel_image to use the new
parameter name.

In `@getstream/chat/client.py`:
- Around line 50-71: The method upload_channel_image uses a parameter named
"type" which conflicts with the Python built-in and causes the same type/name
issue; rename the parameter to a clear identifier (e.g., channel_type) in the
upload_channel_image signature, update all internal references
(path_params={"type": channel_type} and any uses in the method body), and adjust
any callers or tests to pass channel_type instead of type so the path
interpolation "/api/v2/chat/channels/{type}/{id}/image" still receives the
correct value.
- Around line 31-47: The upload_channel_file sync method declares file as
Optional[str] but passes it to _upload_multipart which requires a non-optional
str; change the method signature in upload_channel_file to accept file: str
(remove Optional and default None) so callers must provide a file, update any
related type hints and docs for UploadChannelFileResponse and ensure form_fields
logic remains the same (this mirrors the async version change to enforce a
non-optional file argument).

In `@getstream/common/async_client.py`:
- Around line 26-37: The async upload_file function's signature uses
Optional[str] for file but passes it to _upload_multipart which requires a
non-optional str; update the upload_file declaration to require file: str
(remove Optional) and adjust any callers/tests that passed None, ensuring
upload_file (in getstream.common.async_client) and its use of _upload_multipart
keep consistent types; reference the upload_file method and the
_upload_multipart call so you change the parameter type and any calling code
accordingly (mirror the same fix applied to the sync CommonClient upload_file).
- Around line 40-58: The type annotation for the `file` parameter in async def
upload_image currently uses Optional[str], which is incorrect; change it to
match the accepted upload types used by _upload_multipart (for example
Optional[Union[str, bytes, BinaryIO]] or your project’s UploadFile alias),
update the import(s) for Union/BinaryIO if needed, and ensure the function
signature in upload_image and any call sites align with _upload_multipart’s
expected file type (refer to the upload_image function and the _upload_multipart
usage).

In `@getstream/common/client.py`:
- Around line 26-37: The upload_file signature declares file: Optional[str] but
passes it directly to _upload_multipart which requires a non-optional file_path;
update upload_file to validate and handle None (or make the parameter required).
Either (A) change the type to file: str and remove the default None so callers
must supply a path, or (B) keep Optional[str] and add an explicit check in
upload_file (if file is None: raise ValueError("file must be provided") or
similar) before calling self._upload_multipart("/api/v2/uploads/file",
FileUploadResponse, file, form_fields=...), ensuring the runtime never passes
None to _upload_multipart; adjust the function signature and any callers
accordingly.
- Around line 40-58: The upload_image function's file parameter is incorrectly
typed as Optional[str]; change its annotation to the correct file-like type
(e.g., Optional[IO[bytes]] or Optional[BinaryIO]) to match how _upload_multipart
expects a file object, update any related imports (typing.IO or typing.BinaryIO)
and adjust docstring/comments if present; ensure the signature in upload_image
and any similar functions (e.g., upload_file) use the same file-like type so
callers and type checkers align with _upload_multipart's handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9e2c4896-5308-4b03-ac6c-ab5d1d0e9680

📥 Commits

Reviewing files that changed from the base of the PR and between 64754d9 and 3b33759.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • getstream/base.py
  • getstream/chat/async_client.py
  • getstream/chat/client.py
  • getstream/common/async_client.py
  • getstream/common/client.py
  • tests/test_chat_channel.py
  • tests/test_chat_misc.py

Comment on lines +40 to +58
async def upload_image(
self,
file: Optional[str] = None,
upload_sizes: Optional[List[ImageSize]] = None,
user: Optional[OnlyUserID] = None,
) -> StreamResponse[ImageUploadResponse]:
form_fields = []
if user is not None:
form_fields.append(("user", json.dumps(user.to_dict())))
if upload_sizes is not None:
form_fields.append(
("upload_sizes", json.dumps([s.to_dict() for s in upload_sizes]))
)
return await self._upload_multipart(
"/api/v2/uploads/image",
ImageUploadResponse,
file,
form_fields=form_fields,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Same type issue for upload_image.

Proposed fix
     `@telemetry.operation_name`("getstream.api.common.upload_image")
     async def upload_image(
         self,
-        file: Optional[str] = None,
+        file: str,
         upload_sizes: Optional[List[ImageSize]] = None,
         user: Optional[OnlyUserID] = None,
     ) -> StreamResponse[ImageUploadResponse]:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@getstream/common/async_client.py` around lines 40 - 58, The type annotation
for the `file` parameter in async def upload_image currently uses Optional[str],
which is incorrect; change it to match the accepted upload types used by
_upload_multipart (for example Optional[Union[str, bytes, BinaryIO]] or your
project’s UploadFile alias), update the import(s) for Union/BinaryIO if needed,
and ensure the function signature in upload_image and any call sites align with
_upload_multipart’s expected file type (refer to the upload_image function and
the _upload_multipart usage).

Comment on lines +40 to +58
def upload_image(
self,
file: Optional[str] = None,
upload_sizes: Optional[List[ImageSize]] = None,
user: Optional[OnlyUserID] = None,
) -> StreamResponse[ImageUploadResponse]:
form_fields = []
if user is not None:
form_fields.append(("user", json.dumps(user.to_dict())))
if upload_sizes is not None:
form_fields.append(
("upload_sizes", json.dumps([s.to_dict() for s in upload_sizes]))
)
return self._upload_multipart(
"/api/v2/uploads/image",
ImageUploadResponse,
file,
form_fields=form_fields,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Same type mismatch applies to upload_image.

The file parameter here also has the same Optional[str] type issue as upload_file.

Proposed fix
     `@telemetry.operation_name`("getstream.api.common.upload_image")
     def upload_image(
         self,
-        file: Optional[str] = None,
+        file: str,
         upload_sizes: Optional[List[ImageSize]] = None,
         user: Optional[OnlyUserID] = None,
     ) -> StreamResponse[ImageUploadResponse]:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def upload_image(
self,
file: Optional[str] = None,
upload_sizes: Optional[List[ImageSize]] = None,
user: Optional[OnlyUserID] = None,
) -> StreamResponse[ImageUploadResponse]:
form_fields = []
if user is not None:
form_fields.append(("user", json.dumps(user.to_dict())))
if upload_sizes is not None:
form_fields.append(
("upload_sizes", json.dumps([s.to_dict() for s in upload_sizes]))
)
return self._upload_multipart(
"/api/v2/uploads/image",
ImageUploadResponse,
file,
form_fields=form_fields,
)
def upload_image(
self,
file: str,
upload_sizes: Optional[List[ImageSize]] = None,
user: Optional[OnlyUserID] = None,
) -> StreamResponse[ImageUploadResponse]:
form_fields = []
if user is not None:
form_fields.append(("user", json.dumps(user.to_dict())))
if upload_sizes is not None:
form_fields.append(
("upload_sizes", json.dumps([s.to_dict() for s in upload_sizes]))
)
return self._upload_multipart(
"/api/v2/uploads/image",
ImageUploadResponse,
file,
form_fields=form_fields,
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@getstream/common/client.py` around lines 40 - 58, The upload_image function's
file parameter is incorrectly typed as Optional[str]; change its annotation to
the correct file-like type (e.g., Optional[IO[bytes]] or Optional[BinaryIO]) to
match how _upload_multipart expects a file object, update any related imports
(typing.IO or typing.BinaryIO) and adjust docstring/comments if present; ensure
the signature in upload_image and any similar functions (e.g., upload_file) use
the same file-like type so callers and type checkers align with
_upload_multipart's handling.

Address code review feedback: change `file` from `Optional[str]` to
`str` in all upload method overrides to prevent runtime errors when
None is passed to the multipart upload helper.

Made-with: Cursor
Made-with: Cursor

# Conflicts:
#	getstream/base.py
#	tests/test_chat_misc.py
Format sync and async chat client upload helpers so Ruff format check passes in CI.

Made-with: Cursor
@itsmeadi itsmeadi merged commit 97bb477 into main Apr 1, 2026
30 of 31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants