aiohttp 3.14.4 fixes truncated retried file bodies: Sume submits

aiohttp 3.14.4 fixed an internal retry that sent a cut-off body from a file object. Send a Sume JSON submit from bytes and keep one Idempotency-Key.

4 min readSume
All posts

If you submit a Sume job with aiohttp, build the JSON body in memory and pass the bytes, and upgrade to aiohttp 3.14.4. That release fixed a bug where an internally retried request sent a truncated body when the request data was a file object. A Sume submit body is a few hundred bytes, so there is no reason to hand aiohttp an open file at all.

What 3.14.4 fixed

The aiohttp changelog (read 2026-10-10) dates 3.14.4 to 2026-10-04; the GitHub release page shows the same notes. The items that touch a client calling Sume are below.

aiohttp 3.14.4 changelog items, read 2026-10-10
ItemChangelog wordingReference
Retried file bodiesFixed internally retried requests sending truncated bodies from file objects#13329, #13330
Upload progressAdded aiohttp.UploadTracker; ClientResponse.output_size and upload_complete are deprecated#13579
ConnectorFixed BaseConnector(keepalive_timeout=None) crashing on subsequent requests#13756, #13757

Why a cut-off body is worse than an error

A JSON body that stops halfway is not valid JSON. The errors page lists 400 invalid_request for a body that is not valid, so the good outcome is a clean rejection. The risky case is a retry that you cannot see: your code made one call, the library made two, and you now have to reason about whether the first one created a job.

That is what Idempotency-Key is for. The jobs page says to send the same key on a retried submit so the retry returns the original job instead of billing a second one. Pair that with a body that cannot be half-read.

import asyncio
import json
import os

import aiohttp

URL = os.environ.get("SUME_URL", "https://api.sume.com/v1/videos")


async def main() -> None:
    payload = {"model": "seedance-2.5", "prompt": "비 오는 서울 골목", "duration": 5}
    body = json.dumps(payload, ensure_ascii=False).encode("utf-8")  # bytes in memory
    headers = {
        "Authorization": f"Bearer {os.environ['SUME_API_KEY']}",
        "Content-Type": "application/json",
        "Idempotency-Key": os.environ["RUN_KEY"],  # same key on every retry
    }
    async with aiohttp.ClientSession() as session:
        async with session.post(URL, data=body, headers=headers) as resp:
            job = await resp.json()
            print(resp.status, job.get("id"), job.get("polling_url"))


asyncio.run(main())

Notes on the sample

The request shape follows the video docs: model, prompt, and an integer duration, with the response carrying id and polling_url. I ran the script on aiohttp 3.14.4 against a local stub and it printed the status, job id, and polling URL.

  • Encode once with ensure_ascii=False and .encode("utf-8"). The Content-Type header says application/json, and the bytes are the same on a retry.
  • Read the key from the environment or from your job table. Do not generate a new UUID inside the retry loop, because a new key is a new paid job.
  • Poll polling_url afterward. Do not submit again because a local timeout fired.
  • If you really must upload a large file elsewhere, UploadTracker is the new way to watch progress. Sume's generation endpoints take public HTTPS URLs for media, so a Sume submit does not need it.

Pin aiohttp>=3.14.4 in the service that talks to Sume. The fix is in the library, but the habit of sending bytes you built yourself works on every version.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume