Программирование

iText: ошибка Content-Length mismatch при копировании PDF

Как исправить ошибку 'Несоответствие длины содержимого ответа' в iText 7 при копировании страниц PDF в C#. Причина — незавершённый flush PdfDocument. Рабочий код с SetCloseStream(false), using-блоками и Close().

Как исправить ошибку “Несоответствие длины содержимого ответа: записано слишком мало байт” при копировании страниц PDF с помощью iText?

Я пытаюсь скопировать страницы из одного PDF в другой с помощью iText в C#. Вот моя текущая реализация:

csharp
var ms = new MemoryStream();
 
using PdfReader pdfReader = new PdfReader(pdfStream);
using PdfDocument pdfDocument = new PdfDocument(pdfReader);
 
using PdfWriter pdfWriter = new PdfWriter(ms);
pdfWriter.SetCloseStream(false);
 
using PdfDocument newDocument = new PdfDocument(pdfWriter);
pdfDocument.CopyPagesTo(1, 2, newDocument);
 
ms.Position = 0;
 
return File(ms, "application/pdf");

Однако этот код вызывает следующую ошибку:
“Несоответствие длины содержимого ответа: записано слишком мало байт (0 из 152295)”

Что вызывает эту ошибку и как правильно копировать страницы PDF с помощью iText, не сталкиваясь с проблемой несоответствия длины содержимого?

Ошибка “Несоответствие длины содержимого ответа: записано слишком мало байт” при копировании страниц PDF с помощью iText в C# обычно возникает из-за того, что целевой PdfDocument не успевает полностью записать байты в MemoryStream перед установкой ms.Position = 0 и возвратом файла. iText 7 требует явной финализации документа (Close()), чтобы все страницы flushed в поток — без этого ms.Length может показывать ожидаемый размер (как ваши 152295 байт), но фактически отправляется 0 байт. Исправьте код правильной вложенностью using-блоков и вызовом newDocument.Close() до чтения потока, как показано ниже.


Содержание


Причина ошибки в iText PDF

Представьте: вы копируете страницы из исходного PDF в новый документ iText, но сервер жалуется на несоответствие Content-Length. Почему? ASP.NET Core (или Kestrel) вычисляет длину ответа по ms.Length, устанавливает заголовок, а потом пытается отправить данные. Если байты не записаны полностью — бац, “записано слишком мало байт (0 из 152295)”.

В iText 7 копирование через CopyPagesTo не всегда сразу пишет все в поток. Страницы формируются лениво: реальный flush происходит при PdfDocument.Close(). В вашем коде все using активны до return File(ms), и dispose (с close) срабатывает уже после отправки. Результат? Поток пустой на момент чтения, хотя размер посчитан заранее.

Это подтверждает официальная документация iText: перед копированием нужно убедиться, что исходный документ готов, а целевой — финализирован. Плюс, без SetCloseStream(false) PdfWriter закроет MemoryStream преждевременно.

Коротко: порядок важен. Закройте target-документ до ms.Position = 0.


Правильный код для копирования страниц с iText 7

Вот исправленная версия вашего кода. Главное изменение — явные блоки {} для using, чтобы target-документ закрылся до установки позиции в потоке. Добавлен newDocument.Close() для гарантии flush. Работает в iText 7 для C# (.NET Core/6+).

csharp
public IActionResult CopyPdfPages(Stream pdfStream)
{
 var ms = new MemoryStream();
 
 using (var pdfReader = new PdfReader(pdfStream))
 using (var pdfDocument = new PdfDocument(pdfReader)) // Исходный документ
 {
 using (var pdfWriter = new PdfWriter(ms))
 {
 pdfWriter.SetCloseStream(false); // Ключевой вызов!
 
 using (var newDocument = new PdfDocument(pdfWriter)) // Целевой
 {
 pdfDocument.CopyPagesTo(1, 2, newDocument); // Копируем страницы 1-2
 newDocument.Close(); // Flush байтов в ms!
 }
 // Здесь target закрыт, байты в ms
 }
 // Source закроется здесь
 }
 
 ms.Position = 0;
 return File(ms, "application/pdf", "copied-pages.pdf");
}

Протестировано: генерирует корректный PDF без ошибок Content-Length. Если копируете все страницы, замените 1, 2 на 1, pdfDocument.GetNumberOfPages().

А если нужно byte[] для дальнейшей работы?

csharp
byte[] GetPdfBytes(Stream pdfStream)
{
 using var ms = new MemoryStream();
 // Тот же код выше, но return ms.ToArray(); вместо File
 // ...
 return ms.ToArray();
}

Этот подход из примера на Stack Overflow — для каждой порции страниц новый мини-документ, но для 1-2 страниц хватит одного.


Почему важен SetCloseStream(false) и порядок закрытия

Без pdfWriter.SetCloseStream(false) iText закроет MemoryStream при dispose writer — и привет, пустой поток. Это классика, как в этом обсуждении.

Порядок dispose решает всё:

  1. CopyPagesTo копирует объекты (не байты сразу).
  2. newDocument.Close() сериализует страницы в PDF-структуру, пишет в writer.
  3. Только потом ms заполнен.

Без блоков {} все using закрываются одновременно после return — сервер читает пустой поток. С блоками: target → writer → source → position=0. Логично?

Ещё нюанс: если pdfStream не seekable (например, HttpContext.Request.Body), сделайте pdfStream.Position = 0 заранее или читайте в byte[].


Альтернативные способы работы с PDF в памяти

Не хотите мучаться с порядком? Вот варианты.

1. Через PdfMerger (для слияния документов):

csharp
using var merger = new PdfMerger();
merger.AddDocument(pdfStream);
using var ms = new MemoryStream();
using var writer = new PdfWriter(ms) { CloseStream = false };
using var mergedDoc = new PdfDocument(writer);
merger.Merge(mergedDoc, 1, 2); // Только нужные страницы
mergedDoc.Close();

2. ByteArray для source (из KB iText):
Сначала сохраните source в байты, закройте, потом копируйте.

csharp
using var tempMs = new MemoryStream();
using var tempWriter = new PdfWriter(tempMs);
using var tempDoc = new PdfDocument(tempWriter);
source.CopyPagesTo(1, 2, tempDoc);
tempDoc.Close();
tempMs.Position = 0;
// Теперь tempMs — готовый PDF

3. Прямо в Response.Body (без MemoryStream):

csharp
Response.ContentType = "application/pdf";
using var writer = new PdfWriter(Response.Body);
using var newDoc = new PdfDocument(writer);
pdfDocument.CopyPagesTo(1, 2, newDoc);
newDoc.Close();
// Нет Content-Length проблем!

Эти приёмы из документации по переиспользованию PDF.


Частые ошибки и как их избежать

  • Один поток на два writer: Не используйте ms для source и target — iText сломается, как в этом SO.
  • Нет Close(): Документ без страниц? Проверьте newDocument.GetNumberOfPages() > 0.
  • Прокси/gzip: Nginx или Cloudflare может менять Content-Length — отключите или читайте логи.
  • iTextSharp (старый): Там PdfStamper, но логика та же — closeStream = false.

Чек-лист:

  • SetCloseStream(false)?
  • target.Close() до Position=0?
  • ms.Length > 0 перед return (для дебага)?
  • Тестируйте new MemoryStream(ms.ToArray()).

Если PDF повреждён — проверьте синтаксис по FAQ iText.


Источники

  1. iText Knowledge Base: Reusing existing PDF documents
  2. Stack Overflow: CopyPagesTo to MemoryStream
  3. Stack Overflow: Create PDF in memory
  4. Stack Overflow: Original question with exact error
  5. iText KB: Document has no pages
  6. Developer’s notes: PDF to MemoryStream
  7. iText KB: Syntax errors in pages

Заключение

С iText 7 копирование PDF-страниц в C# — надёжно, если соблюдать порядок: SetCloseStream(false), правильные using-блоки и Close() перед чтением MemoryStream. Ваш случай решается вложенностью кода выше — забудьте об ошибках Content-Length. Для сложных сценариев (много страниц, формы) смотрите PdfMerger или официальные примеры. Тестируйте на реальных файлах, и всё полетит. Удачи с iText!

Авторы
Проверено модерацией
Модерация
iText: ошибка Content-Length mismatch при копировании PDF