РуководстваОпубликовано: 07.11.2026• 0 просмотров

AI API вернул 200, но декодирование не удалось: gzip, повторная распаковка и неверные заголовки

Различайте HTTP-кодирование содержимого, автоматическую распаковку и разбор JSON. Локальные примеры HTTPX помогают найти повторную распаковку, несоответствие заголовков и длины и понять, почему корректный JSON ещё не доказывает целостность gzip.

API может вернуть HTTP 200, а программа затем — DecodingError, Not a gzipped file, ошибку UTF-8 или разбора JSON. Противоречия нет: код статуса можно получить до чтения, распаковки и разбора тела. Сначала определите этап сбоя, чтобы не принять повторную распаковку за неисправность модели и не отправить уже выполненный запрос генерации ещё раз.

Статья посвящена HTTP-сжатию непотоковых JSON-ответов. Среда проверки примеров: Python 3.12.10 и HTTPX 0.28.1; источники проверены 2026-11-07. Это не сравнение скорости провайдеров и не руководство по сборке событий SSE. Все эксперименты выполнены на локальном HTTP-сервисе с самостоятельно подготовленными данными, без обращений к платным API.

1. Что означают три заголовка ответа

ЗаголовокОсновной смыслЧастая ошибка
Content-Type: application/jsonПосле снятия кодирования содержимого данные имеют тип JSONСчитать любые исходные байты текстом UTF-8 лишь потому, что указан JSON
Content-Encoding: gzipК представлению применено кодирование содержимого gzipСчитать, что SDK ещё не распаковал тело, раз заголовок сохранился
Content-LengthВ обычном ответе с телом — длина передаваемого содержимого в байтахПринимать её за размер распакованного JSON или число символов

RFC 9110 различает тип данных, кодирование содержимого и длину.[1] Если передаётся gzip-содержимое, Content-Length обычно соответствует числу сжатых байтов, а не длине распакованной строки. Для HEAD, 304 и других случаев, когда соответствующее тело фактически не передаётся, действуют отдельные правила. При передаче HTTP/1.1 по частям также нельзя требовать обязательного наличия Content-Length.

Transfer-Encoding: chunked относится к формированию сообщения HTTP/1.1 и не означает gzip. «Сырое тело», которое HTTP-клиент отдаёт приложению, обычно уже очищено от транспортного обрамления; это не все байты из сетевого захвата. Число китайских символов, объём UTF-8, размер gzip и общий сетевой трафик — разные величины.

2. Сначала выясните, распакованы ли полученные байты

Официальная документация HTTPX указывает, что обычное содержимое ответа автоматически декодируется для gzip и deflate. Потоковый iter_bytes() также возвращает байты после декодирования содержимого. iter_raw() такого декодирования не выполняет.[2]

Поэтому вызов gzip.decompress для уже прочитанного response.content может закончиться Not a gzipped file. Обычно это означает, что функции передали обычные байты, например JSON; само по себе такое исключение не доказывает, что сервер прислал повреждённый gzip. Сохранившийся заголовок Content-Encoding тоже не доказывает, что приложение получило сжатые байты.

Проверьте цепочку по порядку:

  1. Найдите место первого чтения тела: HTTP-библиотеку, SDK, промежуточный обработчик журналирования, код прокси или собственную функцию gzip. Не ограничивайтесь строкой последней ошибки.
  2. Уточните, что возвращает этот интерфейс: байты после декодирования содержимого, текст или байты с сохранённым кодированием. Соответствующее декодирование должно выполняться ровно один раз.
  3. В обычном обработчике JSON используйте результат штатного декодирования библиотеки. Не распаковывайте его вручную только из-за заголовка gzip.
  4. Чтобы проверить несоответствие заголовков и тела, возьмите небольшой контролируемый образец без чувствительных данных и ещё не прочитанный потоковый ответ с iter_raw(). Прочитанный сетевой поток нельзя считать перематываемым; не повторяйте реальный запрос генерации автоматически ради второй копии данных.

gzip.decompress размещает распакованный результат в памяти.[3] Маленький сжатый файл может превратиться в очень большой. Здесь функция подходит для образцов заранее известного размера. Передача ей целого неизвестного производственного ответа не обеспечивает защиту памяти.

3. Сужайте поиск по этапу сбоя

НаблюдениеЧто проверить сначалаКакого вывода делать не следует
Ошибка возникает только в собственном gzip.decompress, а библиотека уже разбирает JSONНе распаковывается ли декодированное содержимое второй разСжатие на стороне источника обязательно повреждено
HTTPX выдаёт DecodingError при чтении телаСоответствуют ли байты объявленному кодированию, нет ли поврежденияДостаточно сменить модель или ключ
Исходные байты сжаты, но Content-Encoding отсутствуетКакой участок удалил объявление кодирования или отправил неверные метаданныеПри Content-Type JSON кодирование можно игнорировать
Тип JSON указан, но прочитанное тело пустоДопускает ли статус пустое тело и должен ли метод вообще возвращать JSONПустой ответ обязательно связан со сжатием
Несжатое тело вызывает UnicodeDecodeErrorКодировку текста, исходные байты и слой ошибкиПодстановка символов замены исправляет ответ
Соединение завершилось раньше объявленной длиныНе обрезано ли HTTP-сообщение и не сохранил ли шлюз старую длинуДостаточно дописать закрывающую скобку JSON

Тип исключения помогает локализовать сбой, но не назначает виновного. Промежуточный шлюз может изменить тело или заголовки; клиент — способ чтения. Сопоставляйте данные одного и того же участка: статус, Content-Type, Content-Encoding, Content-Length, версию клиента, время и обезличенный идентификатор запроса. Не выводите полный ответ, Authorization, Cookie или учётные данные прокси.

Если вместо JSON получены HTML, страница входа или SSE, сначала разберитесь с фактическим типом данных. Границы событий SSE и признаки завершения разобраны в руководстве по потоковым ответам. Для обращения к платформе используйте шаблон минимального воспроизведения и обезличенного обращения.

4. Небольшая функция для разделения ошибок декодирования, текста и JSON

Функция ниже принимает ещё не прочитанный потоковый HTTPX Response и сама не отправляет запросов. Вызывайте её внутри контекста client.stream: контекст должен закрыть ответ как при успехе, так и при исключении. Клиенту по-прежнему нужны подходящие тайм-ауты подключения, чтения и других этапов, а проверка TLS должна оставаться включённой. Не отправляйте задачу генерации повторно автоматически после ошибки функции.

Пример намеренно принимает только непотоковый JSON без сжатия, с identity или с одним слоем gzip. Это не универсальный распаковщик: br, zstd, deflate и несколько слоёв кодирования явно отклоняются. Такой отказ не означает, что эти варианты нарушают HTTP. Допуск application/*+json тоже говорит только о подходящем типе данных, а не об успешности операции.

import json
import httpx

class ResponseCheckError(ValueError):
    pass

def read_json_response(response, max_decoded=65536):
    if type(max_decoded) is not int or max_decoded <= 0:
        raise ValueError("max_decoded must be a positive integer")
    if not 200 <= response.status_code < 300:
        raise ResponseCheckError(f"HTTP_{response.status_code}")
    media = response.headers.get("content-type", "").split(";", 1)[0].strip().lower()
    if not (media == "application/json" or
            (media.startswith("application/") and media.endswith("+json"))):
        raise ResponseCheckError("NOT_JSON_MEDIA_TYPE")
    coding = response.headers.get("content-encoding", "").strip().lower()
    if coding not in ("", "identity", "gzip"):
        raise ResponseCheckError("UNSUPPORTED_CONTENT_ENCODING")

    body = bytearray()
    try:
        for chunk in response.iter_bytes():
            if len(body) + len(chunk) > max_decoded:
                raise ResponseCheckError("DECODED_BODY_TOO_LARGE")
            body.extend(chunk)
    except httpx.DecodingError:
        raise ResponseCheckError("CONTENT_DECODING_FAILED") from None

    if not body:
        raise ResponseCheckError("EMPTY_JSON_BODY")
    try:
        text = body.decode("utf-8")
    except UnicodeDecodeError:
        raise ResponseCheckError("INVALID_UTF8") from None
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        raise ResponseCheckError("INVALID_JSON") from None

В примере max_decoded равен 65 536 байтам. Это предел декодированного тела, которое функция сохраняет и передаёт для разбора текста; меняйте его по контракту интерфейса, а не воспринимайте как рекомендованный лимит модели. Сжатый Content-Length не подменяет эту проверку. Ошибки UTF-8 не игнорируются ради видимости исправного ответа.

У функции два существенных ограничения. Во-первых, нижележащий декодер может сначала выделить большой блок, а разбор JSON создаёт дополнительные объекты. Поэтому это не жёсткий предел памяти процесса и не полноценная защита от «архива-бомбы». Для больших недоверенных ответов нужны отдельно обеспечиваемые бюджеты сжатых и распакованных данных, ограничения параллелизма и изоляция ресурсов. Во-вторых, функция использует декодер HTTPX и не гарантирует дополнительно целостность формата gzip.

ReadTimeout, RemoteProtocolError и другие сетевые исключения передаются вызывающему коду, а не превращаются в INVALID_JSON. Возвращённый объект ещё нужно проверить по контракту API: ошибки операции, поля, состояние завершения и требуемую Schema. Здесь сохранена и стандартная семантика Python json.loads: пример дополнительно не запрещает повторяющиеся ключи или нестандартные числовые константы и не является строгим валидатором JSON Schema.

5. Успешный разбор JSON не всегда означает целостность сжатого ответа

В локальном эксперименте мы создали gzip с корректным JSON, удалили последние восемь байт завершающей части gzip и установили HTTP Content-Length равным фактически отправляемой укороченной длине. В HTTPX 0.28.1 именно этот образец всё ещё выдавал полные байты JSON, и приведённая функция их разбирала. В то же время Python gzip.decompress для того же маленького исходного образца выдавал EOFError.

Это не означает, что проходит любое усечение или что «при нехватке восьми байт ответ всегда восстанавливается». В другом образце исходный Content-Length сохранялся, но соединение закрывалось раньше — HTTPX выдавал RemoteProtocolError. При повреждении контрольной суммы gzip возникал DecodingError. Результат может зависеть от места обрыва, сочетания кодирований и версии клиента.

Реализация gzip-декодера HTTPX 0.28.1 использует zlib и flush, но не выполняет здесь дополнительную проверку состояния конца gzip-потока.[4] Поэтому отдельно проверяйте три условия: завершилось ли HTTP-сообщение согласно своему обрамлению, целостен ли формат кодирования содержимого и выполнены ли условия завершения операции в декодированном ответе. Корректный синтаксис JSON покрывает лишь часть этих требований.

Всего успешно выполнено 25 локальных проверок: обычный и gzip-ответ, китайский текст в UTF-8, тип данных, пустое тело, HTTP-ошибки, несоответствие заголовков и тела, повреждение контрольной суммы, два варианта усечения, повторная распаковка, сырые и декодированные байты, границы лимита, закрытие при ошибке и успешный следующий запрос. Реальные платформы, TLS, HTTP/2, производительность и тарификация не тестировались. Результаты эксперимента не выдаются за доказательство неисправности провайдера.

6. При изменении тела шлюз должен согласовать метаданные

Если пересылающий слой получает от HTTPX уже декодированное тело, но без изменений копирует исходные Content-Encoding: gzip и Content-Length, следующий клиент может попытаться распаковать обычный JSON или ожидать данные по устаревшей длине. Это возможный механизм сбоя, который нужно подтвердить наблюдениями реальной цепочки; одного исключения для такого вывода недостаточно.

Сопровождающему шлюз следует выбрать и проверить согласованный способ передачи:

  • С сохранением кодирования содержимого: тело остаётся в соответствующем кодировании, а объявления соответствуют фактическим данным. Обрамлением передачи для текущего соединения занимается серверный фреймворк; нельзя бездумно копировать заголовки, действующие только на одном участке соединения.
  • С декодированным телом: ответ больше не объявляется исходным gzip; подходящая длина рассчитывается заново или определяется фреймворком. Если следующий слой снова сжимает данные, именно он должен сформировать соответствующие метаданные кодирования и длины.

Удалить только Content-Encoding, сохранив сжатую длину, или изменить только Content-Length, оставив неверное кодирование, — неполное исправление. Сжатие могут независимо выполнять фреймворк, обратный прокси и кэш. Проверяйте реально развёрнутую цепочку, а не угадывайте результат для клиента по одному фрагменту кода.

Если контракт интерфейса и сетевая политика допускают это, сравните ответы одного безопасного диагностического адреса без учётных данных, меняя только Accept-Encoding: identity. Заголовок выражает предпочтение ответа без кодирования содержимого, но не заменяет проверку фактических заголовков и тела.[1] Не отключайте из-за такого эксперимента сжатие на всём сайте и не превращайте проверку в автоматический повтор исходного платного POST.

Приёмку исправления проводите в исходной среде сбоя: проверьте обычный ответ, ошибку, пустое тело, сжатую и несжатую передачу, а также фактические условия завершения операции. Ошибка разбора не доказывает, что источник не выполнил запрос и не начислил оплату. Перед повтором сопоставьте идентификатор запроса с записями начислений; поможет руководство по сверке запросов и счёта.

7. Первичные источники

Материалы проверены 2026-11-07. Выводы о реализации HTTPX относятся к использованной в эксперименте версии 0.28.1; одинаковое поведение других версий не предполагается.

Опубликовано: 7 ноября 2026 г.
最后更新: 07.11.2026

Похожие статьи

Руководства

Длинный ID из API изменился во фронтенде? Точность JavaScript и передача строк

Изменились последние цифры ID, поиск вернул 404 или номера в счёте слились? Проверьте текст JSON, разбор, хранение и повторную отправку; узнайте, почему String и BigInt иногда уже не помогут, и проверьте передачу строковых ID на локальных примерах.

Руководства

Как считать стабильность API: исходные запросы, повторы и конечная успешность

Успех после повтора нельзя считать новым независимым успехом. Статья разделяет request_id, попытки и конечные состояния, объясняет границы 429, 503, тайм-аутов и идемпотентности, а локальный скрипт считает конечную успешность и усиление повторами.

Руководства

API вернул 202 без результата: состояния асинхронных заданий, опрос и повторная отправка

Различайте приём задания, успешную проверку статуса и завершение работы. Сохраняйте ID, ограничивайте опрос и разбирайте 404, истечение срока результата и неизвестный исход отправки без повторного создания задания.

Отказ от ответственности

Данные о сервисах и ценах вводятся вручную; доступность сайтов проверяется автоматически с указанием времени последней проверки.Информация о провайдерах может меняться, поэтому перед оплатой рекомендуем посетить официальный сайт сервиса. Мы не гарантируем качество услуг сторонних провайдеров.

© 2026 Выбор API. All rights reserved.