Skip to content

fix: properly unpack _execute() tuple in drafts.create() and drafts.update() for multipart requests#459

Open
dudegladiator wants to merge 3 commits into
nylas:mainfrom
dudegladiator:fix/drafts-multipart-tuple-unpacking
Open

fix: properly unpack _execute() tuple in drafts.create() and drafts.update() for multipart requests#459
dudegladiator wants to merge 3 commits into
nylas:mainfrom
dudegladiator:fix/drafts-multipart-tuple-unpacking

Conversation

@dudegladiator
Copy link
Copy Markdown

@dudegladiator dudegladiator commented Feb 20, 2026

Fixes #454

Summary

When creating or updating a draft with attachments larger than 3MB (which triggers multipart/form-data upload), the SDK throws a TypeError: tuple indices must be integers or slices, not str.

Root Cause

_http_client._execute() returns a tuple (json_response, headers) (via _validate_response in http_client.py line 48). However, the multipart code paths in drafts.create() and drafts.update() assigned the entire tuple to a single variable instead of unpacking it:

# Before (broken) — json_response is actually (dict, headers) tuple
json_response = self._http_client._execute(...)
return Response.from_dict(json_response, Draft)  # TypeError

The send() method already did this correctly:

# send() — correct pattern
json_response, headers = self._http_client._execute(...)
return Response.from_dict(json_response, Message, headers)

Fix

Applied the same tuple-unpacking pattern to both create() and update() multipart code paths in nylas/resources/drafts.py:

create():

json_response, headers = self._http_client._execute(
    method="POST",
    path=path,
    data=_build_form_request(request_body),
    overrides=overrides,
)
return Response.from_dict(json_response, Draft, headers)

update():

json_response, headers = self._http_client._execute(
    method="PUT",
    path=path,
    data=_build_form_request(request_body),
    overrides=overrides,
)
return Response.from_dict(json_response, Draft, headers)

Changes

  • nylas/resources/drafts.py — 4 lines changed (2 in create(), 2 in update())
  • CHANGELOG.md — entry added under Unreleased
  • tests/resources/test_drafts.py — 2 regression tests added (see below)

Testing

Regression tests (added in this PR)

Two new tests in tests/resources/test_drafts.py exercise the real Response.from_dict code path (no patch on Response) with _execute returning a realistic (dict, headers) tuple, then assert the call returns a Response[Draft] with the expected data, request_id, and headers:

  • test_create_draft_large_attachment_unpacks_execute_tuple
  • test_update_draft_large_attachment_unpacks_execute_tuple

I verified that reverting the fix in nylas/resources/drafts.py causes both new tests to fail with the exact TypeError: tuple indices must be integers or slices, not str from issue #454, confirming they regression-cover the bug. Reapplying the fix → both pass.

The pre-existing test_create_draft_with_special_characters_large_attachment did not catch this bug because it patched Response.from_dict and only asserted on _execute call args, not on processing of the return value.

Full suite

============== 449 passed, 3 deselected, 1 warning in 1.66s ==============

Manual verification against Nylas sandbox

Earlier verified end-to-end against a real Microsoft (Outlook) grant by creating and sending a draft with a real 7MB PDF attachment via drafts.create() (and via the create → send flow). Both succeeded without TypeError, confirming the fix works on the wire.


I confirm that this contribution is made under the terms of the MIT license and that I have the authority necessary to make this contribution on behalf of its copyright owner.

…pdate() for multipart requests

When creating or updating a draft with attachments >= 3MB (multipart/form-data),
_http_client._execute() returns a (json_response, headers) tuple. The multipart
code paths in create() and update() assigned the entire tuple to a single variable
instead of unpacking it, causing TypeError.

Applied the same tuple-unpacking pattern already used in send() to both create()
and update() multipart code paths.

Fixes nylas#454
@dudegladiator
Copy link
Copy Markdown
Author

@pengfeiye @benjaminwhtan @AaronDDM
Could you please let me know when you plan to merge these changes? 🤔

@AaronDDM AaronDDM self-assigned this May 27, 2026
@AaronDDM AaronDDM self-requested a review May 27, 2026 13:39
Copy link
Copy Markdown
Contributor

@AaronDDM AaronDDM left a comment

Choose a reason for hiding this comment

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

@dudegladiator sorry for the delay and thank you for your contribution!

The fix is correct — _execute() returns (json_response, headers) and drafts.create()/drafts.update() were treating the tuple as a dict. Aligning these two paths with the existing send() pattern is the right call, and forwarding headers to Response.from_dict matches that pattern.

Two things missing before this can merge:

  1. No CHANGELOG entry. The repo has an Unreleased section at the top of CHANGELOG.md — please add a bullet there, e.g.:

    * Fixed TypeError in `drafts.create()` and `drafts.update()` when attachments trigger the multipart code path (>3MB)
    
  2. No tests committed. The PR body describes 8 unit tests and 2 integration tests, but changedFiles: 1 and the diff only touches nylas/resources/drafts.py. The existing test_create_draft_with_special_characters_large_attachment in tests/resources/test_drafts.py mocks _execute in a way that didn't catch this regression — it only asserts on call args, not on processing of the return value. Please commit at least one regression test where the mocked _execute returns a realistic (dict, headers) tuple and asserts the call returns a Response[Draft] without raising. Same for update().

Once those two are in I'm happy to approve.

@dudegladiator
Copy link
Copy Markdown
Author

@AaronDDM thanks for the review — both points addressed in the latest commit:

  1. CHANGELOG.md — added a bullet under Unreleased:

    Fixed TypeError in drafts.create() and drafts.update() when attachments trigger the multipart code path (>3MB) — _execute() returns a (json_response, headers) tuple, and both methods now unpack it correctly and forward headers to Response.from_dict

  2. Regression tests — added two tests in tests/resources/test_drafts.py:

    • test_create_draft_large_attachment_unpacks_execute_tuple
    • test_update_draft_large_attachment_unpacks_execute_tuple

    These don't patch Response.from_dict (unlike the existing test_create_draft_with_special_characters_large_attachment, which is why it didn't catch this regression). Each builds a Mock _http_client whose _execute.return_value is a realistic (dict, headers) tuple, then asserts the call returns a Response[Draft] with the expected data.id, request_id, and headers.

    I verified the regression coverage by reverting the fix in drafts.py — both new tests fail with the exact TypeError: tuple indices must be integers or slices, not str from Bug: drafts.create() fails with TypeError for multipart requests (attachments > 3MB) #454. Reapplying the fix → both pass. Full suite: 449 passed.

PR description updated to reflect what's actually in the diff. Ready for another look.

@dudegladiator dudegladiator requested a review from AaronDDM June 5, 2026 04:20
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.

Bug: drafts.create() fails with TypeError for multipart requests (attachments > 3MB)

2 participants