iText: ошибка Content-Length mismatch при копировании PDF
Как исправить ошибку 'Несоответствие длины содержимого ответа' в iText 7 при копировании страниц PDF в C#. Причина — незавершённый flush PdfDocument. Рабочий код с SetCloseStream(false), using-блоками и Close().
Как исправить ошибку “Несоответствие длины содержимого ответа: записано слишком мало байт” при копировании страниц PDF с помощью iText?
Я пытаюсь скопировать страницы из одного PDF в другой с помощью iText в C#. Вот моя текущая реализация:
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
- Правильный код для копирования страниц с iText 7
- Почему важен SetCloseStream(false) и порядок закрытия
- Альтернативные способы работы с PDF в памяти
- Частые ошибки и как их избежать
- Источники
- Заключение
Причина ошибки в 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+).
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[] для дальнейшей работы?
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 решает всё:
CopyPagesToкопирует объекты (не байты сразу).newDocument.Close()сериализует страницы в PDF-структуру, пишет в writer.- Только потом
msзаполнен.
Без блоков {} все using закрываются одновременно после return — сервер читает пустой поток. С блоками: target → writer → source → position=0. Логично?
Ещё нюанс: если pdfStream не seekable (например, HttpContext.Request.Body), сделайте pdfStream.Position = 0 заранее или читайте в byte[].
Альтернативные способы работы с PDF в памяти
Не хотите мучаться с порядком? Вот варианты.
1. Через PdfMerger (для слияния документов):
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 в байты, закройте, потом копируйте.
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):
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.
Источники
- iText Knowledge Base: Reusing existing PDF documents
- Stack Overflow: CopyPagesTo to MemoryStream
- Stack Overflow: Create PDF in memory
- Stack Overflow: Original question with exact error
- iText KB: Document has no pages
- Developer’s notes: PDF to MemoryStream
- iText KB: Syntax errors in pages
Заключение
С iText 7 копирование PDF-страниц в C# — надёжно, если соблюдать порядок: SetCloseStream(false), правильные using-блоки и Close() перед чтением MemoryStream. Ваш случай решается вложенностью кода выше — забудьте об ошибках Content-Length. Для сложных сценариев (много страниц, формы) смотрите PdfMerger или официальные примеры. Тестируйте на реальных файлах, и всё полетит. Удачи с iText!