Transmitir un PDF grande como respuesta HTTP generada
De un vistazo
Sección titulada «De un vistazo»Se genera un PDF grande dentro de un controlador y se quieren devolver los bytes sin retener una segunda copia completa en el búfer de la respuesta. Cada integración de framework incluye una variante transmitida de su fábrica PdfResponse: streamInline() y streamDownload(). Cada una devuelve un StreamedResponse del framework cuya devolución de llamada escribe el cuerpo del PDF al cliente en fragmentos fijos de 64 KB.
Conviene leer con cuidado el modelo de memoria antes de elegir este camino. El motor construye primero el documento completo en memoria. La devolución de llamada transmitida invoca getPdfData(), que materializa todo el PDF como una sola cadena y luego recorre esa cadena en porciones de 64 KB. El pico que se ahorra es la segunda copia que un Illuminate\Http\Response o Symfony\Component\HttpFoundation\Response con búfer retendría mientras el framework mide Content-Length. La variante transmitida no mide la longitud, por lo que omite Content-Length. Nunca retiene el cuerpo de la respuesta y la cadena del documento al mismo tiempo. No es transmisión incremental real: NextPDF no tiene una superficie de escritura incremental, así que el documento se materializa por completo antes de que el primer byte llegue al socket.
Requisitos previos, declarados desde el inicio para evitar sorpresas a mitad de la tarea:
- El core de NextPDF está instalado y una integración de framework,
nextpdf/laravelonextpdf/symfony, está instalada y descubierta. - Ya se sabe cómo enrutar una solicitud hacia un controlador en el framework.
- Ya se leyó Devolver un PDF generado desde un controlador, que cubre las fábricas con búfer
inline()ydownload()sobre las que se construye esta recipe.
Esta guía práctica se centra en el patrón StreamedResponse que comparten Laravel y Symfony. CodeIgniter 4 incluye los mismos nombres de método streamInline() / streamDownload(), pero envuelve los bytes en un CodeIgniter\HTTP\DownloadResponse en lugar de un StreamedResponse impulsado por una devolución de llamada. La sección de casos límite deja constancia de esa diferencia.
Instalación
Sección titulada «Instalación»Instala la integración que coincida con el framework. Ejecuta uno de los siguientes comandos.
composer require nextpdf/laravelcomposer require nextpdf/symfonyEn Laravel, publica la configuración después de instalar.
php artisan vendor:publish --tag=nextpdf-configSymfony registra el bundle automáticamente a través de Flex. Confirma el descubrimiento en la página de instalación del framework antes de continuar.
Resumen conceptual
Sección titulada «Resumen conceptual»Una fábrica de respuestas con búfer, PdfResponse::download() o PdfResponse::inline(), invoca getPdfData(), almacena la cadena devuelta en un objeto Response y establece Content-Length a partir de strlen(). Entonces, el framework retiene esa cadena durante toda la vida de la respuesta. En un documento grande, eso significa que la cadena del documento y la cadena del cuerpo de la respuesta conviven en memoria.
La fábrica transmitida adopta una forma distinta. PdfResponse::streamDownload() y PdfResponse::streamInline() devuelven un StreamedResponse construido con una devolución de llamada. El framework invoca esa devolución de llamada solo cuando está listo para enviar el cuerpo. Dentro de la devolución de llamada, la integración invoca getPdfData() una vez, divide la cadena devuelta en fragmentos de 64 KB y hace echo de cada fragmento seguido de un flush(). No se retiene ninguna segunda copia persistente del cuerpo ni se emite la cabecera Content-Length.
Dos hechos dan forma a cada decisión en esta página:
- La construcción es ansiosa, la transferencia es fragmentada.
getPdfData()enNextPDF\Core\Documentinvoca al escritor y devuelve todo el PDF como una sola cadena. La fragmentación de 64 KB solo gobierna cómo salen del proceso los bytes ya construidos. La memoria pico está acotada por el tamaño de un documento terminado, no por una pequeña ventana de transmisión. - Sin
Content-Length. La variante transmitida no puede conocer la longitud del cuerpo sin construirlo dentro de la devolución de llamada, así que omite la cabecera. Una barra de progreso del cliente, una solicitudRangeo un proxy sensible a la longitud no verán ningún tamaño. Eligedownload()/inline()con búfer cuando una longitud conocida importe más que ahorrar la copia de respuesta.
Obtén el documento a través de la ruta de resolución idiomática del framework:
- Laravel: resuelve
NextPDF\Contracts\DocumentFactoryInterfacedesde el contenedor e invocacreate(). Devuelve unNextPDF\Core\Documentnuevo, el tipo concreto que aceptan las fábricas transmitidas. - Symfony: inyecta
NextPDF\Symfony\Service\PdfFactorye invocacreate(). Devuelve unNextPDF\Core\Documentnuevo con los valores predeterminados configurados ya aplicados.
Superficie de la API
Sección titulada «Superficie de la API»| Asunto | Laravel | Symfony |
|---|---|---|
| Documento nuevo | app(DocumentFactoryInterface::class)->create() | PdfFactory::create() |
| Inline transmitido | PdfResponse::streamInline($doc, $name) | PdfResponse::streamInline($doc, $name) |
| Descarga transmitida | PdfResponse::streamDownload($doc, $name) | PdfResponse::streamDownload($doc, $name) |
| Tipo devuelto | Symfony\Component\HttpFoundation\StreamedResponse | Symfony\Component\HttpFoundation\StreamedResponse |
| Llamada de construcción dentro de la devolución de llamada | NextPDF\Core\Document::getPdfData() | NextPDF\Core\Document::getPdfData() |
| Tamaño de fragmento | 64 KB (str_split determinista) | 64 KB (bucle substr determinista) |
El PdfResponse de Laravel vive en NextPDF\Laravel\Http\PdfResponse; el de Symfony, en NextPDF\Symfony\Http\PdfResponse. Sus fábricas transmitidas devuelven ambas el mismo tipo Symfony\Component\HttpFoundation\StreamedResponse. Ambas aplican el mismo conjunto fijo de cabeceras de endurecimiento de respuesta del Open Web Application Security Project (OWASP) (X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Content-Security-Policy: default-src 'none', X-Robots-Tag: noindex, nofollow, Referrer-Policy: no-referrer) y ambas sanean el nombre de archivo de la descarga. No es necesario agregar esas cabeceras manualmente.
Ambas fábricas invocan la misma superficie core subyacente, NextPDF\Core\Document::getPdfData(): string, que construye y devuelve todo el binario del PDF. Su homólogo save(string $path): void escribe los mismos bytes en disco mediante un escritor atómico. Esta recipe usa getPdfData() porque el destino es un socket HTTP, no un archivo.
Ejemplo de código — Inicio rápido
Sección titulada «Ejemplo de código — Inicio rápido»Esta es la acción mínima de descarga transmitida en cada framework. Las llamadas al documento usan la misma superficie core; solo difiere el andamiaje del controlador. La fábrica transmitida entrega al framework una devolución de llamada, por lo que la acción retorna de inmediato. El cuerpo se construye y se vacía cuando el framework envía la respuesta.
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use NextPDF\Contracts\DocumentFactoryInterface;use NextPDF\Laravel\Http\PdfResponse;use Symfony\Component\HttpFoundation\StreamedResponse;
final class ReportController extends Controller{ public function annualReport(): StreamedResponse { $document = app(DocumentFactoryInterface::class)->create(); $document->addPage(); $document->cell(0, 10, 'Annual report', newLine: true);
return PdfResponse::streamDownload($document, 'annual-report.pdf'); }}<?php
declare(strict_types=1);
namespace App\Controller;
use NextPDF\Symfony\Http\PdfResponse;use NextPDF\Symfony\Service\PdfFactory;use Symfony\Component\HttpFoundation\StreamedResponse;use Symfony\Component\Routing\Attribute\Route;
final class ReportController{ #[Route('/report', name: 'report_pdf')] public function annualReport(PdfFactory $pdf): StreamedResponse { $document = $pdf->create(); $document->addPage(); $document->cell(0, 10, 'Annual report', newLine: true);
return PdfResponse::streamDownload($document, 'annual-report.pdf'); }}Para previsualizar en una pestaña del navegador en lugar de forzar una descarga, invoca streamInline(...) en lugar de streamDownload(...). El Content-Disposition pasa a ser inline y todas las demás cabeceras permanecen iguales.
Ejemplo de código — Producción
Sección titulada «Ejemplo de código — Producción»Una acción de producción inyecta sus dependencias, valida la entrada de la ruta, captura la excepción más específica que la construcción puede lanzar, registra la clase de fallo sin filtrar una traza y devuelve un error HTTP definido. El ejemplo siguiente usa inyección por constructor de Laravel. El equivalente en Symfony sigue la misma forma, con PdfFactory inyectado por acción.
getPdfData() se ejecuta dentro de la devolución de llamada transmitida, así que cualquier excepción que lance aparece después de que el framework haya empezado a enviar las cabeceras. Para que el manejo de errores siga siendo significativo, construye el documento (el paso que puede fallar) antes de devolver la respuesta y captura allí el fallo de construcción. Solo la transferencia fragmentada de los bytes ya construidos ocurre después dentro de la devolución de llamada.
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Response;use NextPDF\Contracts\DocumentFactoryInterface;use NextPDF\Core\Document;use NextPDF\Exception\NextPdfException;use NextPDF\Laravel\Http\PdfResponse;use Psr\Log\LoggerInterface;use Symfony\Component\HttpFoundation\StreamedResponse;
final class StatementController extends Controller{ private const int MAX_STATEMENT_ID = 9_999_999;
public function __construct( private readonly DocumentFactoryInterface $documents, private readonly LoggerInterface $logger, ) {}
public function show(int $statementId): StreamedResponse|Response { // Validate input at the boundary before any build work runs. if ($statementId < 1 || $statementId > self::MAX_STATEMENT_ID) { return new Response('Invalid statement identifier.', 422); }
try { // Build the whole document up front. getPdfData(), invoked inside // the streamed callback, materializes the full PDF in memory, so // do the failure-prone build here, where the catch can still set a // clean HTTP status before any byte is sent. $document = $this->buildStatement($statementId); $document->getPdfData(); } catch (NextPdfException $exception) { // Log the exception class, never the message or a stack trace, so // internal detail does not leak into the log sink. $this->logger->error('Statement PDF build failed', [ 'statement_id' => $statementId, 'exception' => $exception::class, ]);
return new Response('Could not generate the statement PDF.', 500); }
// The build succeeded. The streamed factory rebuilds the bytes inside // its callback and flushes them to the client in 64 KB chunks. return PdfResponse::streamDownload( $document, "statement-{$statementId}.pdf", ); }
private function buildStatement(int $statementId): Document { $document = $this->documents->create(); $document->addPage(); $document->cell(0, 10, "Statement #{$statementId}", newLine: true);
return $document; }}Captura NextPDF\Exception\NextPdfException, la base abstracta que extienden todas las excepciones de NextPDF, cuando se necesite un único manejador para cualquier fallo de construcción. Para reaccionar ante causas específicas, captura primero los subtipos concretos que getPdfData() puede lanzar: NextPDF\Exception\PageLayoutException cuando el contenido no cabe en la geometría de la página, NextPDF\Exception\CompressionException cuando falla la compresión del flujo, y NextPDF\Exception\InvalidConfigException para una configuración de salida inválida. Nunca escribas un bloque catch vacío. Cada rama aquí registra la clase de fallo y devuelve un estado definido.
Resolver un documento nuevo por acción mantiene la fábrica intercambiable en las pruebas. No reutilices una sola instancia de controlador para dos documentos no relacionados dentro de un único proceso de worker de larga duración, porque se arrastraría estado de contenido obsoleto.
Casos límite y trampas
Sección titulada «Casos límite y trampas»- El documento se construye dos veces en el patrón de validar-y-luego-transmitir. El ejemplo de producción invoca
getPdfData()una vez para validar la construcción y luego la fábrica lo invoca de nuevo dentro de la devolución de llamada. Ese es el costo de mover el punto de fallo antes de las cabeceras. Cuando una doble construcción resulte demasiado costosa para un documento dado, omite la sonda de preconstrucción y acepta que un fallo de construcción dentro de la devolución de llamada truncará una respuesta ya iniciada. - Sin
Content-Length. La variante transmitida omite la cabecera. Las barras de progreso de descarga y las solicitudesRangeno funcionarán. Usadownload()/inline()con búfer cuando se requiere una longitud conocida. - Un proxy con búfer anula el beneficio. Un proxy inverso o un búfer de salida de PHP que captura todo el cuerpo antes de reenviarlo vuelve a retener el PDF completo y elimina la copia ahorrada. Configura el proxy para transmitir las respuestas
application/pdf, o usa una respuesta con búfer en esa ruta. - CodeIgniter 4 no es de transmisión por devolución de llamada. La integración de CodeIgniter incluye los mismos nombres de método
streamInline()/streamDownload(), pero devuelven unCodeIgniter\HTTP\DownloadResponseque retiene el cuerpo completo, no unStreamedResponseimpulsado por una devolución de llamada. El patrón StreamedResponse de esta página aplica solo a Laravel y Symfony. - No escribas en el cuerpo después de retornar. La devolución de llamada transmitida es dueña de la salida. No hagas
echoni escribas manualmente en el cuerpo de la respuesta después de devolver elStreamedResponseal framework. - Los documentos firmados fallan rápido. Invocar
getPdfData()en un documento configurado para una firma PAdES de alto nivel lanzaNextPDF\Exception\NotImplementedExceptionen lugar de emitir un archivo sin firmar. Transmite la salida firmada a través de la ruta de firma documentada, no a través de esta recipe.
Rendimiento
Sección titulada «Rendimiento»La transmisión acota la copia de la respuesta, no la construcción del documento. La memoria pico es aproximadamente el tamaño de un PDF terminado, porque getPdfData() materializa todo el documento antes de que se envíe el primer fragmento. En un documento realmente grande o de varias páginas, la construcción en sí, no la transferencia, domina el presupuesto de la solicitud. Mueve la generación fuera del hilo de la solicitud con un trabajo en cola. Consulta Generar un PDF en un trabajo en cola.
El tamaño de fragmento de 64 KB es fijo y determinista en ambas integraciones. Solo gobierna la granularidad de la transferencia y no cambia el total de bytes enviados ni la memoria pico. Elige la variante transmitida cuando la copia de respuesta ahorrada sea la restricción y no se requiera una barra de progreso. Elige la variante con búfer para respuestas pequeñas y sensibles a la latencia que se beneficien de un Content-Length conocido.
Notas de seguridad
Sección titulada «Notas de seguridad»- Valida la entrada antes de construir. La acción de producción rechaza un identificador fuera de rango con un
422antes de que se ejecute cualquier trabajo de construcción. Nunca interpoles entrada no validada en la construcción ni en el nombre de archivo. - El saneamiento del nombre de archivo se aplica por ti. Ambas fábricas transmitidas sanean el nombre de archivo y añaden el conjunto de cabeceras de endurecimiento de respuesta de OWASP. Pasa un valor bajo control propio y deja que la fábrica lo sanee como segunda capa. No codifiques el nombre de archivo manualmente.
- Acota la memoria concurrente. Como todo el PDF se materializa en memoria por solicitud, un tráfico concurrente elevado multiplica la memoria pico. Impón límites de tamaño y de tasa sobre las entradas que impulsan una construcción para mitigar la denegación de servicio por agotamiento de memoria.
- Registra la clase de fallo, no el mensaje. El bloque catch registra
$exception::classy un identificador de correlación, nunca el mensaje de la excepción ni una traza de pila. Una traza sin procesar en un colector de registros es una fuga de información. - Sin catch vacío. Cada rama catch en esta página registra y devuelve una respuesta de error definida.
Conformidad
Sección titulada «Conformidad»Esta guía no hace ninguna afirmación normativa de estándares. Cada clase, método y cabecera mostrados son la superficie pública verificada de la integración nombrada: NextPDF\Core\Document::getPdfData(), las fábricas transmitidas NextPDF\Laravel\Http\PdfResponse y NextPDF\Symfony\Http\PdfResponse, y el tipo de retorno Symfony\Component\HttpFoundation\StreamedResponse. La semántica de las cabeceras de endurecimiento de respuesta de OWASP que aplican las fábricas está documentada, con sus citas, en la página de seguridad y operaciones de cada integración, enlazada en Consulta también. Esta página del cookbook reformula el uso y delega las citas normativas a esas páginas.
Consulta también
Sección titulada «Consulta también»- Devolver un PDF generado desde un controlador: las contrapartes con búfer
inline()ydownload(). - Generar un PDF en un trabajo en cola: mueve la construcción fuera del hilo de la solicitud.
- Uso en producción de Laravel: controlador cableado con DI, conjunto de cabeceras de OWASP y contrato de enlace del contenedor.
- Uso en producción de Symfony: la devolución de llamada transmitida, el emisor de fragmentos de 64 KB y el localizador del constructor.