generate_content_stream fails with UnknownApiResponseError when a streamed text part contains U+2028 / U+2029 / U+0085 (httpx transport)

Ouverte
#3,243 1 commentaire 0 réactions 1 personne assignée Voir sur GitHub

Les mainteneurs répondent en général sous 1 jour

@Venkaiahbabuneelam y travaille déjà.

Depuis le 29/10/2026.

  • #3245 par @copybara-service — ouverte

Évaluation

Cette issue n'a pas encore été évaluée.

Description

priority: p2 status:awaiting user response type: bug
Environment details
  • Programming language: Python
  • OS: Windows 11 (transport-level, not OS-specific)
  • Language runtime version: 3.14
  • Package version: google-genai 2.33.0 (code unchanged on main)
Symptom

If a streamed chunk's JSON contains a raw U+2028 (LINE SEPARATOR), U+2029 or U+0085 (NEL) inside a string, models.generate_content_stream (sync) and aio.models.generate_content_stream with an httpx client raise:

UnknownApiResponseError: Failed to parse response as JSON. Raw response: {"candidates": [{"content": {"role": "model", "parts": [{"text": "line one

The same bytes parse correctly on the aiohttp async path.

Steps to reproduce

Self-contained script with a local SSE server, no API key (below):

sync  (httpx)   ERR UnknownApiResponseError ... "text": "line one
async (httpx)   ERR UnknownApiResponseError ... "text": "line one
async (aiohttp) ['line one
line two', ' done']

The body is valid JSON (RFC 8259 doesn't require escaping these code points), one data: line per event.

repro_genai_linesep.py
"""google-genai: streamed text containing U+2028 / U+2029 / U+0085 breaks SSE parsing (httpx path)."""
import asyncio, json, sys, threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import httpx
from google import genai

TEXT = sys.argv[1].encode().decode("unicode_escape") if len(sys.argv) > 1 else "line one\u2028line two"
CHUNKS = [
    {"candidates": [{"content": {"role": "model", "parts": [{"text": TEXT}]}}]},
    {"candidates": [{"content": {"role": "model", "parts": [{"text": " done"}]}, "finishReason": "STOP"}]},
]
# Valid JSON (RFC 8259 does not require escaping these code points); one event per line.
BODY = b"".join(b"data: " + json.dumps(c, ensure_ascii=False).encode() + b"\r\n\r\n" for c in CHUNKS)

class H(BaseHTTPRequestHandler):
    def do_POST(self):
        self.rfile.read(int(self.headers["Content-Length"]))
        self.send_response(200)
        self.send_header("Content-Type", "text/event-stream")
        self.send_header("Content-Length", str(len(BODY)))
        self.end_headers()
        self.wfile.write(BODY)
    def log_message(self, *a): pass

srv = ThreadingHTTPServer(("127.0.0.1", 0), H)
threading.Thread(target=srv.serve_forever, daemon=True).start()
base = f"http://127.0.0.1:{srv.server_port}"
print(f"genai {genai.__version__}, text={TEXT!r}")

def run_sync():
    c = genai.Client(api_key="test", http_options={"base_url": base})
    return [ch.text for ch in c.models.generate_content_stream(model="gemini-2.5-flash", contents="hi")]

async def run_async(httpx_client):
    opts = {"base_url": base}
    if httpx_client:
        opts["httpx_async_client"] = httpx.AsyncClient()
    c = genai.Client(api_key="test", http_options=opts)
    return [ch.text async for ch in await c.aio.models.generate_content_stream(model="gemini-2.5-flash", contents="hi")]

for name, fn in [("sync  (httpx)  ", run_sync),
                 ("async (httpx)  ", lambda: asyncio.run(run_async(True))),
                 ("async (aiohttp)", lambda: asyncio.run(run_async(False)))]:
    try:
        print(name, fn())
    except Exception as e:
        print(name, "ERR", type(e).__name__, str(e)[:160].replace("\n", " "))

Root cause

google/genai/_api_client.py:488 (iter_lines()) and :545 (aiter_lines()) use httpx's line decoder, which follows str.splitlines() and also breaks on U+2028/U+2029/U+0085 (and , , -). The SSE spec treats only CRLF, LF and CR as line terminators. The aiohttp branch (:587, readline() on bytes) gets this right.

Suggested fix

Split the byte stream on `

/
/
only (e.g. iterateiter_bytes()/aiter_bytes()` through a small line splitter, as the aiohttp branch effectively does), then decode each line.

Langage dominant
Python
Étoiles
4.1k
Forks
1.1k
Merge moyen
1 j 20 h
PR mergées (30 j)
108

Préparer son environnement

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de googleapis/python-genai

Toutes les issues de googleapis/python-genai

Issues similaires

Plus d'issues Python

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.