Generación por lotes en Connect con seguimiento de progreso
En resumen
Sección titulada «En resumen»Procesa una lista de documentos hasta completarla desde un único proceso cliente en NextPDF Connect, la distribución de servicio HTTP independiente del motor. Esta recipe envía cada solicitud de renderizado al endpoint de trabajos asíncronos POST /api/v1/jobs, consulta cada trabajo con GET /api/v1/jobs/{id} hasta que alcanza un estado terminal, lee los campos status y progress que informa el servidor para cada trabajo, y descarga cada PDF completado desde GET /api/v1/jobs/{id}/result.
El ciclo de vida del trabajo es fijo y pequeño. Un trabajo está pending, luego running, y luego exactamente en un estado terminal: completed, failed o cancelled. La respuesta de estado incluye un entero progress de 0 a 100 cuando el servidor lo rastrea, y un encabezado Retry-After en cada sondeo no terminal que indica cuánto esperar antes de la siguiente solicitud. Identificar cada envío con un Idempotency-Key hace que un envío reintentado devuelva el mismo trabajo en lugar de iniciar un segundo renderizado.
Esta recipe sigue el camino directo a nivel de protocolo, sin ocultar el transporte. Usa la superficie REST directamente y no asume ningún kit de desarrollo de software (SDK) específico de un lenguaje, así que el mismo flujo se puede trasladar a cualquier cliente HTTP.
Instalación
Sección titulada «Instalación»En el servidor se usa la distribución estándar de Connect:
composer require nextpdf/serverEl cliente PHP del ejemplo de producción que aparece más abajo usa un cliente HTTP y fábricas de mensajes que cumplen con PSR-18 y PSR-17. Instala las implementaciones que el proyecto ya utilice como estándar, por ejemplo:
composer require psr/http-client psr/http-factoryVisión conceptual
Sección titulada «Visión conceptual»La superficie de trabajos asíncronos separa el envío de la recuperación. No se mantiene una conexión HTTP larga abierta por documento. En su lugar, se envía un trabajo, se recibe un identificador y se consulta un endpoint de estado ligero hasta que el trabajo termina. Esa forma hace que un lote sea manejable: el cliente rastrea N trabajos independientes a la vez sin N conexiones bloqueadas.
Tres endpoints llevan el flujo:
POST /api/v1/jobsacepta el mismo cuerpo de solicitud de renderizado que el endpoint síncrono/api/v1/render: unpage_size, unaorientationy un arreglo ordenadooperations. Devuelve201 Createdpara un trabajo nuevo, o200 OKcuando unIdempotency-Keycoincide con un trabajo ya enviado.GET /api/v1/jobs/{id}devuelve el registro actual del trabajo. Para un trabajo no terminal también establece un encabezadoRetry-After(el servidor usa un intervalo de 2 segundos) y un campopoll_url. Respeta el encabezado en lugar de sondear en un bucle cerrado.GET /api/v1/jobs/{id}/resulttransmite el PDF terminado comoapplication/pdf. Devuelve409 Conflictsi el trabajo no ha alcanzadocompleted, así que debe llamarse solo una vez que el sondeo de estado confirme el estado terminal.
Cada respuesta correcta comparte un mismo envoltorio: un objeto data con los campos del trabajo, y un objeto meta con el request_id, timestamp, duration_ms y api_version. Los campos del trabajo que se leen viven bajo data: data.status, data.progress, data.job_id y, en un trabajo completado, data.result_url.
Una advertencia sobre la versión actual. El servidor procesa un trabajo enviado en línea antes de responder al POST, así que en la práctica la respuesta del envío ya lleva un status terminal, y el resultado puede estar listo en el primer sondeo. El contrato de sondeo y progreso documentado aquí es la forma estable de la API. El servidor lo mantiene sin cambios a medida que el backend de procesamiento pasa a un grupo de workers en cola, así que un cliente escrito para sondear es correcto hoy y seguirá siéndolo después de ese cambio. Debe implementarse el bucle de sondeo. No debe asumirse que la primera respuesta es no terminal, ni tampoco que es terminal.
Superficie de la API
Sección titulada «Superficie de la API»La superficie REST de trabajos asíncronos de Connect, tal como la definen el documento OpenAPI del servidor y el enrutamiento de JobHandler:
POST /api/v1/jobs: envía un trabajo de renderizado. Encabezado de solicitudIdempotency-Keyopcional. El cuerpo es una solicitud de renderizado (operationses obligatorio y debe contener al menos una operación). Respuestas:201nuevo,200repetición idempotente,422cuerpo inválido,409conflicto de idempotencia,429por límite de tasa.GET /api/v1/jobs/{id}: consulta el estado. Respuesta200con el registro del trabajo; encabezadoRetry-Afterpresente mientras el trabajo no es terminal;404si el trabajo no existe o pertenece a otro cliente.GET /api/v1/jobs/{id}/result: descarga el PDF.200application/pdfcuando estácompleted;409cuando aún no está completado;404si el trabajo es desconocido.DELETE /api/v1/jobs/{id}: cancela un trabajopendingorunning, o elimina unocompleted(204).
El registro del trabajo bajo data lleva estos campos, exactamente como el servidor los serializa.
job_id: el identificador (un prefijojob_y 24 caracteres hexadecimales).status: uno depending,running,completed,failed,cancelled. Los dos primeros son no terminales; los tres últimos son terminales.created_aty, una vez establecidos,started_atycompleted_at: marcas de tiempo ISO-8601.progress: un entero de 0 a 100, presente solo cuando el servidor lo rastrea para el trabajo; ausente (tratarlo como desconocido) en caso contrario.error: una cadena de mensaje, presente solo en un trabajofailed.result_url: presente solo en un trabajocompleted; la ruta a la descarga del resultado.poll_url: presente solo mientras el trabajo es no terminal.
La autenticación es un token de portador en el encabezado Authorization: Authorization: Bearer npk_live_{kid}_{secret}.
Ejemplo de código — Inicio rápido
Sección titulada «Ejemplo de código — Inicio rápido»Este ejemplo lleva un trabajo de principio a fin a nivel de protocolo para mostrar las tres llamadas y los campos que devuelven. Envía, sondea una vez y descarga. El ejemplo de producción de más abajo añade el bucle por lotes, la espera de Retry-After y el manejo completo de errores.
# 1. Submit an async render job. Capture the job_id from data.job_id.curl -sS -X POST "$NEXTPDF_CONNECT_URL/api/v1/jobs" \ -H "Authorization: Bearer $NEXTPDF_CONNECT_TOKEN" \ -H 'Content-Type: application/json' \ -H "Idempotency-Key: invoice-2026-04-0001" \ -d '{"page_size":"A4","orientation":"portrait","operations":[{"type":"add_text","text":"Invoice 0001"}]}'
# 2. Poll status. Read data.status and data.progress; honour Retry-After.curl -sS "$NEXTPDF_CONNECT_URL/api/v1/jobs/job_0123456789abcdef01234567" \ -H "Authorization: Bearer $NEXTPDF_CONNECT_TOKEN"
# 3. Once data.status is "completed", download the PDF binary.curl -sS "$NEXTPDF_CONNECT_URL/api/v1/jobs/job_0123456789abcdef01234567/result" \ -H "Authorization: Bearer $NEXTPDF_CONNECT_TOKEN" \ -o invoice-0001.pdfEjemplo de código — Producción
Sección titulada «Ejemplo de código — Producción»Este es un cliente autónomo. Envía un lote de solicitudes de renderizado, limita cuántos trabajos hay en curso a la vez, sondea cada trabajo con la cadencia que el servidor solicita mediante Retry-After, informa el progress reportado por el servidor, descarga cada PDF completado y registra los fallos. Usa un cliente HTTP PSR-18 y fábricas PSR-17 (el contrato de transporte que las recipes de Connect adoptan como estándar), y captura la excepción más específica que cada llamada puede lanzar: Psr\Http\Client\ClientExceptionInterface para un fallo de transporte, y una BatchJobException tipada para una respuesta del servidor con la que el lote no puede continuar. Ningún bloque catch está vacío. Cada uno registra y vuelve a lanzar, o bien registra un resultado definido.
Sustituye la lista $documents en línea por las entradas propias. Inyecta el cliente HTTP y las fábricas concretas del proyecto donde el constructor espera las interfaces PSR.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use Psr\Http\Client\ClientExceptionInterface;use Psr\Http\Client\ClientInterface;use Psr\Http\Message\RequestFactoryInterface;use Psr\Http\Message\StreamFactoryInterface;
/** * Raised when a Connect job response prevents the batch from proceeding. * * Distinct from the PSR-18 transport exception: this means the request was * delivered and the server answered, but the answer is one the batch * cannot act on (a non-success status code, or a job that ended in a * terminal failure). */final class BatchJobException extends RuntimeException{}
/** * Drives a batch of async render jobs over the NextPDF Connect REST surface. * * The client submits each render request, polls every job on the cadence * the server requests through Retry-After, and downloads each completed * PDF. It enforces bounded concurrency so a large batch never opens more * in-flight jobs than the host should track at once. */final readonly class ConnectBatchRunner{ /** * @param non-empty-string $baseUrl Connect base URL, no trailing slash * @param non-empty-string $bearerToken Connect API key (npk_live_...) * @param positive-int $maxInFlight Concurrent jobs cap * @param positive-int $maxPolls Per-job poll attempts before giving up */ public function __construct( private ClientInterface $httpClient, private RequestFactoryInterface $requestFactory, private StreamFactoryInterface $streamFactory, private string $baseUrl, private string $bearerToken, private int $maxInFlight = 8, private int $maxPolls = 150, ) {}
/** * Render every document in the batch and write each completed PDF. * * @param array<non-empty-string, array<string, mixed>> $documents * Map of stable document key to render request body. The key * doubles as the Idempotency-Key, so a re-run of the same batch * does not duplicate server-side work. * @param non-empty-string $outputDir Directory for the written PDFs * * @throws BatchJobException When the batch cannot proceed at all * @throws ClientExceptionInterface When the transport cannot send a request * * @return array<non-empty-string, string> Map of document key to a * human-readable outcome line */ public function run(array $documents, string $outputDir): array { $this->assertWritableDir($outputDir);
$outcomes = [];
// Process in bounded windows so the in-flight job count never // exceeds the configured cap, regardless of batch size. foreach (array_chunk($documents, $this->maxInFlight, preserve_keys: true) as $window) { $jobIds = [];
foreach ($window as $key => $body) { $jobIds[$key] = $this->submit($key, $body); }
foreach ($jobIds as $key => $jobId) { $record = $this->pollToTerminal($jobId); $outcomes[$key] = $this->finish($key, $record, $outputDir); } }
return $outcomes; }
/** * Submit one render job and return its identifier. * * @param non-empty-string $idempotencyKey Stable per-document key * @param array<string, mixed> $body Render request body * * @throws BatchJobException * @throws ClientExceptionInterface * * @return non-empty-string The job_id from data.job_id */ private function submit(string $idempotencyKey, array $body): string { $request = $this->requestFactory ->createRequest('POST', $this->baseUrl . '/api/v1/jobs') ->withHeader('Authorization', 'Bearer ' . $this->bearerToken) ->withHeader('Content-Type', 'application/json') ->withHeader('Idempotency-Key', $idempotencyKey) ->withBody($this->streamFactory->createStream($this->encode($body)));
$response = $this->httpClient->sendRequest($request); $status = $response->getStatusCode();
// 201 new job; 200 idempotent replay. Anything else stops the batch. if ($status !== 201 && $status !== 200) { throw new BatchJobException( sprintf('Submit for "%s" returned HTTP %d.', $idempotencyKey, $status), ); }
$data = $this->decodeData($response->getBody()->__toString()); $jobId = $data['job_id'] ?? null;
if (!is_string($jobId) || $jobId === '') { throw new BatchJobException( sprintf('Submit for "%s" returned no job_id.', $idempotencyKey), ); }
return $jobId; }
/** * Poll one job until it reaches a terminal state. * * Honours the Retry-After header on every non-terminal poll. Gives up * after maxPolls attempts and reports the wait as a failure so the * batch records it rather than blocking forever. * * @param non-empty-string $jobId * * @throws BatchJobException * @throws ClientExceptionInterface * * @return array<string, mixed> The terminal job record (data object) */ private function pollToTerminal(string $jobId): array { $url = $this->baseUrl . '/api/v1/jobs/' . rawurlencode($jobId);
for ($attempt = 0; $attempt < $this->maxPolls; $attempt++) { $request = $this->requestFactory ->createRequest('GET', $url) ->withHeader('Authorization', 'Bearer ' . $this->bearerToken);
$response = $this->httpClient->sendRequest($request); $status = $response->getStatusCode();
if ($status !== 200) { throw new BatchJobException( sprintf('Poll for job "%s" returned HTTP %d.', $jobId, $status), ); }
$data = $this->decodeData($response->getBody()->__toString()); $jobStatus = is_string($data['status'] ?? null) ? $data['status'] : 'unknown'; $progress = is_int($data['progress'] ?? null) ? $data['progress'] : null;
$this->logProgress($jobId, $jobStatus, $progress);
// Terminal states: completed, failed, cancelled. if (in_array($jobStatus, ['completed', 'failed', 'cancelled'], strict: true)) { return $data; }
// Non-terminal: wait the interval the server asked for. $this->waitRetryAfter($response->getHeaderLine('Retry-After')); }
throw new BatchJobException( sprintf('Job "%s" did not finish within %d polls.', $jobId, $this->maxPolls), ); }
/** * Act on a terminal job record: download a completed PDF, or report. * * @param non-empty-string $key Document key * @param array<string, mixed> $record Terminal job record (data object) * @param non-empty-string $outputDir Where to write the PDF * * @throws BatchJobException * @throws ClientExceptionInterface * * @return string A human-readable outcome line */ private function finish(string $key, array $record, string $outputDir): string { $jobStatus = is_string($record['status'] ?? null) ? $record['status'] : 'unknown'; $jobId = is_string($record['job_id'] ?? null) ? $record['job_id'] : '';
if ($jobStatus !== 'completed') { // A failed job carries an error message; surface it, do not swallow. $error = is_string($record['error'] ?? null) ? $record['error'] : 'no detail';
return sprintf('%s -> %s (%s)', $key, $jobStatus, $error); }
$path = rtrim($outputDir, '/\\') . DIRECTORY_SEPARATOR . $key . '.pdf'; $this->download($jobId, $path);
return sprintf('%s -> completed, written to %s', $key, $path); }
/** * Download a completed job result and write it to a server-derived path. * * @param non-empty-string $jobId * @param non-empty-string $path Caller-controlled output path * * @throws BatchJobException * @throws ClientExceptionInterface */ private function download(string $jobId, string $path): void { $request = $this->requestFactory ->createRequest('GET', $this->baseUrl . '/api/v1/jobs/' . rawurlencode($jobId) . '/result') ->withHeader('Authorization', 'Bearer ' . $this->bearerToken);
$response = $this->httpClient->sendRequest($request);
if ($response->getStatusCode() !== 200) { throw new BatchJobException( sprintf('Result download for job "%s" returned HTTP %d.', $jobId, $response->getStatusCode()), ); }
$bytes = $response->getBody()->__toString();
if (!str_starts_with($bytes, '%PDF')) { throw new BatchJobException( sprintf('Result for job "%s" is not a PDF.', $jobId), ); }
if (file_put_contents($path, $bytes) === false) { throw new BatchJobException(sprintf('Could not write result to "%s".', $path)); } }
/** * Sleep for the server-requested interval, with a safe floor and ceiling. */ private function waitRetryAfter(string $retryAfter): void { $seconds = ctype_digit($retryAfter) ? (int) $retryAfter : 2; // Clamp to a sane band so a hostile header cannot stall or busy-loop. $seconds = max(1, min(30, $seconds)); sleep($seconds); }
/** * Emit a progress line. Replace with your logger. */ private function logProgress(string $jobId, string $jobStatus, ?int $progress): void { $pct = $progress === null ? 'n/a' : $progress . '%'; fwrite(STDERR, sprintf("[%s] status=%s progress=%s\n", $jobId, $jobStatus, $pct)); }
/** * Decode a response envelope and return its data object. * * @throws BatchJobException When the body is not the expected envelope * * @return array<string, mixed> */ private function decodeData(string $json): array { try { /** @var mixed $decoded */ $decoded = json_decode($json, true, 32, JSON_THROW_ON_ERROR); } catch (JsonException $e) { throw new BatchJobException('Response body is not valid JSON.', previous: $e); }
if (!is_array($decoded) || !isset($decoded['data']) || !is_array($decoded['data'])) { throw new BatchJobException('Response is missing the data envelope.'); }
/** @var array<string, mixed> $data */ $data = $decoded['data'];
return $data; }
/** * @param array<string, mixed> $body * * @throws BatchJobException */ private function encode(array $body): string { try { return json_encode($body, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES); } catch (JsonException $e) { throw new BatchJobException('Render request body is not encodable.', previous: $e); } }
/** * @param non-empty-string $dir * * @throws BatchJobException */ private function assertWritableDir(string $dir): void { if (!is_dir($dir) || !is_writable($dir)) { throw new BatchJobException(sprintf('Output directory "%s" is not writable.', $dir)); } }}
// ---------------------------------------------------------------------------// Wiring. Provide your project's concrete PSR-18 client and PSR-17 factories.// ---------------------------------------------------------------------------
/** @var ClientInterface $httpClient *//** @var RequestFactoryInterface $requestFactory *//** @var StreamFactoryInterface $streamFactory */
$baseUrl = getenv('NEXTPDF_CONNECT_URL');$token = getenv('NEXTPDF_CONNECT_TOKEN');
if ($baseUrl === false || $baseUrl === '' || $token === false || $token === '') { fwrite(STDERR, "Set NEXTPDF_CONNECT_URL and NEXTPDF_CONNECT_TOKEN.\n"); exit(2);}
/** @var array<non-empty-string, array<string, mixed>> $documents */$documents = [ 'invoice-0001' => [ 'page_size' => 'A4', 'orientation' => 'portrait', 'operations' => [ ['type' => 'add_text', 'text' => 'Invoice 0001'], ], ], 'invoice-0002' => [ 'page_size' => 'A4', 'orientation' => 'portrait', 'operations' => [ ['type' => 'add_text', 'text' => 'Invoice 0002'], ], ],];
$runner = new ConnectBatchRunner( httpClient: $httpClient, requestFactory: $requestFactory, streamFactory: $streamFactory, baseUrl: rtrim($baseUrl, '/'), bearerToken: $token, maxInFlight: 8,);
try { $outcomes = $runner->run($documents, getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: sys_get_temp_dir());} catch (BatchJobException $e) { fwrite(STDERR, 'Batch stopped: ' . $e->getMessage() . "\n"); exit(1);} catch (ClientExceptionInterface $e) { fwrite(STDERR, 'Transport failure: ' . $e->getMessage() . "\n"); exit(1);}
foreach ($outcomes as $line) { echo $line, "\n";}STDOUT esperado, una línea por documento; las rutas dependen de tu directorio de salida:
invoice-0001 -> completed, written to /tmp/invoice-0001.pdfinvoice-0002 -> completed, written to /tmp/invoice-0002.pdfCasos límite y trampas
Sección titulada «Casos límite y trampas»- Lee los campos del trabajo bajo
data, no en el nivel superior. Cada respuesta correcta viene dentro de un envoltorio{ "data": ..., "meta": ... }.data.statusydata.progressson los campos sobre los que se actúa;metallevarequest_idpara la correlación de soporte. progresspuede estar ausente. El servidor incluyeprogresssolo cuando lo rastrea para ese trabajo. Trata un campo ausente como «desconocido», no como cero, y guía el bucle porstatus, que siempre está presente.- El envío puede ser ya terminal. En la versión actual el servidor renderiza en línea antes de responder al
POST, así que la respuesta del envío puede llevarstatus: completedy el resultado puede estar listo en el primer sondeo. El bucle de sondeo debe aceptar un estado terminal en el intento cero en lugar de insistir primero en unpending. - Respeta
Retry-After. Las respuestas de estado no terminales establecenRetry-After(un intervalo de 2 segundos). Sondear más rápido desperdicia solicitudes e invita a un429. Acota el valor a un rango sensato en lugar de confiar en él ciegamente. /resultantes de completarse es un409. Llama al endpoint de resultado solo después de que el sondeo de estado muestrecompleted. Un409 Conflictsignifica que el trabajo no está terminado; no es un error de transporte.- Idempotency-Key evita el trabajo duplicado. Un envío reintentado con la misma clave devuelve el trabajo original (
200en lugar de201). Usa una clave estable por documento para que un reintento de red nunca inicie un segundo renderizado. Una clave reutilizada con un cuerpo diferente es un conflicto409. - Los trabajos están limitados al propietario. Un trabajo enviado con una clave de API es invisible para otra; un
GETentre propietarios distintos devuelve404, no403. Sondea con la misma credencial con la que se hizo el envío. - Un trabajo
failedlleva un mensajeerror. Leedata.erroren un estado terminalfailedy regístralo. No reintentes a ciegas.
Rendimiento
Sección titulada «Rendimiento»El costo de un lote es la suma de los renderizados más la sobrecarga del sondeo. Dos variables controlan el lado del cliente. Primero, acotar la concurrencia: el tope maxInFlight fija cuántos trabajos se rastrean a la vez, lo que mantiene estable el número de solicitudes abiertas y la memoria del cliente sin importar el tamaño del lote. Ajústalo para que coincida con el número de workers del servidor, no por encima; tener más trabajos en curso que workers solo alarga el tiempo de cola de cada trabajo. Segundo, respetar el intervalo de sondeo: cada sondeo es una lectura de estado ligera, pero un bucle cerrado multiplica el volumen de solicitudes y activa el limitador de tasa. El intervalo Retry-After de 2 segundos del servidor es el valor predeterminado correcto, y el runner lo acota a un rango de 1 a 30 segundos para que un único trabajo lento no pueda entrar en un bucle activo ni detener la ventana.
Para lotes muy grandes, procesa en ventanas (el runner usa array_chunk) en lugar de enviar todo por adelantado. Eso acota tanto el estado rastreado del cliente como la profundidad de la cola del servidor, así que un lote malformado o sobredimensionado falla dentro de una ventana en lugar de después de miles de envíos.
Notas de seguridad
Sección titulada «Notas de seguridad»- Mantén el token de portador fuera de los registros y las URL. La clave de API viaja únicamente en el encabezado
Authorization. Nunca la coloques en una cadena de consulta, una línea de registro o un artefacto persistido. El runner registra eljob_idy elstatus, nunca la credencial. - Deriva las rutas de salida de claves controladas por el cliente. El runner construye cada ruta de salida a partir de la clave de documento que eligió el código, unida a un directorio de salida fijo, nunca a partir de un valor en una respuesta del servidor. No interpoles un campo del trabajo en una ruta del sistema de archivos, lo que abriría un recorrido de rutas.
- Valida los bytes descargados. El runner comprueba un
200de/resulty el encabezado%PDFantes de escribir el archivo. Un estado de descarga correcto no prueba por sí solo que el cuerpo sea un PDF. - Trata el resultado como no confiable hasta inspeccionarlo. Un trabajo completado significa que el servidor renderizó bytes, no que esos bytes sean seguros para reenviar. Pasa los resultados por un paso de inspección estructural antes de entregarlos a un cliente o sistema posterior.
- Usa una clave de privilegio mínimo. La superficie de trabajos asíncronos es renderizado de nivel Core. Asigna al lote una clave limitada estrictamente a las operaciones que necesita, y rótala según el calendario que establezca la política de gestión de secretos.
- Acota el presupuesto de sondeo.
maxPollsevita que un trabajo atascado retenga al cliente para siempre. El lote registra el tiempo de espera agotado como un resultado en lugar de bloquearse, lo que evita que un trabajo defectuoso niegue el servicio al resto.
Conformidad
Sección titulada «Conformidad»Esta recipe no hace ninguna afirmación normativa de estándares. Consume los endpoints REST de trabajos asíncronos de NextPDF Connect (POST /api/v1/jobs, GET /api/v1/jobs/{id}, GET /api/v1/jobs/{id}/result) y lee los campos del registro del trabajo que el servidor define (status, progress, error, result_url, poll_url). La comprobación del encabezado %PDF en un resultado descargado confirma únicamente que la respuesta comienza con el marcador PDF; no es una determinación de validez ni de conformidad. Para una comprobación de estándares sobre un conjunto de documentos, usa la herramienta de cumplimiento por lotes Enterprise. Consulta Comprobación de estándares por lotes sobre Connect, una superficie distinta de los trabajos de renderizado que se cubren aquí.
Véase también
Sección titulada «Véase también»- Hola mundo sobre Connect: el renderizado individual mínimo antes de procesar por lotes.
- Convenciones de recipes de Connect: el contrato de transporte, autenticación y conformidad que comparte cada recipe de Connect.
- Manejo de errores con reconocimiento de excepciones sobre Connect: cómo el servidor informa errores y cómo debería reaccionar un cliente.
- Comprobación de estándares por lotes sobre Connect: la superficie de cumplimiento Enterprise, distinta de estos trabajos de renderizado.