Batch-PDF-Generierung mit Fortschrittsverfolgung über Connect
Auf einen Blick
Abschnitt betitelt „Auf einen Blick“Führen Sie eine Liste von Dokumenten aus einem einzigen Client-Prozess über NextPDF Connect bis zum Abschluss – die eigenständige HTTP-Service-Distribution der Engine. Dieses Recipe reicht jede Render-Anfrage am Endpunkt für asynchrone Jobs POST /api/v1/jobs ein, fragt jeden Job mit GET /api/v1/jobs/{id} ab, bis er einen Endzustand erreicht, liest die pro Job vom Server gemeldeten Felder status und progress und lädt jedes fertige PDF von GET /api/v1/jobs/{id}/result herunter.
Der Lebenszyklus eines Jobs ist fest und überschaubar. Ein Job ist pending, dann running und anschließend in genau einem Endzustand: completed, failed oder cancelled. Die Statusantwort enthält eine Ganzzahl progress von 0 bis 100, sofern der Server sie verfolgt, sowie bei jeder nicht-terminalen Abfrage einen Retry-After-Header, der angibt, wie lange Sie bis zur nächsten Anfrage warten sollen. Versehen Sie jede Einreichung mit einem Idempotency-Key, damit eine erneut gesendete Einreichung denselben Job zurückgibt, statt einen zweiten Render zu starten.
Dieses Recipe beschreibt den Ablauf auf Protokollebene und bleibt dabei dem Transportverhalten treu. Es nutzt die REST-Fläche direkt und setzt kein sprachspezifisches Software Development Kit (SDK) voraus, sodass sich derselbe Ablauf auf jeden HTTP-Client übertragen lässt.
Installation
Abschnitt betitelt „Installation“Die Serverseite ist die Standard-Connect-Distribution:
composer require nextpdf/serverDer PHP-Client im Produktionsbeispiel unten nutzt einen Hypertext-Transfer-Protocol-Client (HTTP) und Message-Factories, die zu PSR-18 und PSR-17 konform sind. Installieren Sie die Implementierungen, auf die sich Ihr Projekt ohnehin festgelegt hat, zum Beispiel:
composer require psr/http-client psr/http-factoryKonzeptioneller Überblick
Abschnitt betitelt „Konzeptioneller Überblick“Die Fläche für asynchrone Jobs trennt Einreichung von Abruf. Sie halten nicht pro Dokument eine lange HTTP-Verbindung offen. Stattdessen reichen Sie einen Job ein, erhalten einen Bezeichner und fragen einen günstigen Status-Endpunkt ab, bis der Job fertig ist. Genau diese Form macht einen Batch handhabbar: Der Client verfolgt N unabhängige Jobs gleichzeitig, ohne dass N Verbindungen blockiert sind.
Drei Endpunkte tragen den Ablauf:
POST /api/v1/jobsakzeptiert denselben Render-Anfrage-Body wie der synchrone Endpunkt/api/v1/render: einpage_size, eineorientationund ein geordnetesoperations-Array. Er gibt201 Createdfür einen neuen Job zurück oder200 OK, wenn einIdempotency-Keyzu einem Job passt, den Sie bereits eingereicht haben.GET /api/v1/jobs/{id}gibt den aktuellen Job-Datensatz zurück. Für einen nicht-terminalen Job setzt er außerdem einenRetry-After-Header (der Server verwendet ein Intervall von 2 Sekunden) und einpoll_url-Feld. Halten Sie sich an den Header, statt in einer engen Schleife abzufragen.GET /api/v1/jobs/{id}/resultstreamt das fertige PDF alsapplication/pdf. Er gibt409 Conflictzurück, wenn der Job noch nichtcompletederreicht hat; rufen Sie ihn also erst auf, wenn die Statusabfrage den Endzustand bestätigt.
Jede erfolgreiche Antwort verwendet denselben Umschlag: ein data-Objekt mit den Job-Feldern und ein meta-Objekt mit der request_id, dem timestamp, dem duration_ms und der api_version. Die Job-Felder, die Sie lesen, liegen unter data: data.status, data.progress, data.job_id und bei einem fertigen Job data.result_url.
Ein ehrlicher Vorbehalt zum aktuellen Release: Der Server verarbeitet einen eingereichten Job inline, bevor er den POST beantwortet. Daher trägt die Einreichungsantwort in der Praxis bereits einen terminalen status, und das Ergebnis kann schon bei der ersten Abfrage bereitstehen. Der hier dokumentierte Vertrag aus Abfrage und Fortschritt ist die stabile API-Form. Der Server behält ihn unverändert bei, während das verarbeitende Backend auf einen warteschlangenbasierten Worker-Pool umzieht. Ein Client, der auf Abfragen ausgelegt ist, ist damit heute korrekt und bleibt es auch nach dieser Änderung. Schreiben Sie die Poll-Schleife. Gehen Sie nicht davon aus, dass die erste Antwort nicht-terminal ist, aber auch nicht davon, dass sie terminal ist.
API-Fläche
Abschnitt betitelt „API-Fläche“Die REST-Fläche der asynchronen Jobs von Connect ist im OpenAPI-Dokument des Servers und im JobHandler-Routing wie folgt definiert:
POST /api/v1/jobs: einen Render-Job einreichen. OptionalerIdempotency-Key-Request-Header. Der Body ist eine Render-Anfrage (operationsist erforderlich und muss mindestens eine Operation enthalten). Antworten:201neu,200idempotente Wiederholung,422ungültiger Body,409Idempotenzkonflikt,429Ratenbegrenzung.GET /api/v1/jobs/{id}: den Status abfragen. Antwort200mit dem Job-Datensatz; derRetry-After-Header ist vorhanden, solange der Job nicht-terminal ist;404, wenn der Job nicht existiert oder einem anderen Client gehört.GET /api/v1/jobs/{id}/result: das PDF herunterladen.200application/pdf, wenncompleted;409, wenn noch nicht abgeschlossen;404, wenn unbekannt.DELETE /api/v1/jobs/{id}: einenpending- oderrunning-Job abbrechen oder einencompleted-Job löschen (204).
Der Job-Datensatz unter data trägt diese Felder, genau so, wie der Server sie serialisiert.
job_id: der Bezeichner (einjob_-Präfix und 24 hexadezimale Zeichen).status: einer vonpending,running,completed,failed,cancelled. Die ersten beiden sind nicht-terminal; die letzten drei sind terminal.created_atund, sobald gesetzt,started_atundcompleted_at: ISO-8601-Zeitstempel.progress: eine Ganzzahl von 0 bis 100, nur vorhanden, wenn der Server sie für den Job verfolgt; andernfalls fehlend (als unbekannt behandeln).error: ein Nachrichten-String, nur bei einemfailed-Job vorhanden.result_url: nur bei einemcompleted-Job vorhanden; der Pfad zum Ergebnis-Download.poll_url: nur vorhanden, solange der Job nicht-terminal ist.
Die Authentifizierung erfolgt über ein Bearer-Token im Authorization-Header: Authorization: Bearer npk_live_{kid}_{secret}.
Codebeispiel – Schnellstart
Abschnitt betitelt „Codebeispiel – Schnellstart“Dies führt einen Job auf Protokollebene von Anfang bis Ende, sodass Sie die drei Aufrufe und die Felder sehen, die sie zurückgeben. Der Schnellstart reicht ein, fragt einmal ab und lädt herunter. Das Produktionsbeispiel unten ergänzt die Batch-Schleife, die Retry-After-Wartezeit und die vollständige Fehlerbehandlung.
# 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.pdfCodebeispiel – Produktion
Abschnitt betitelt „Codebeispiel – Produktion“Dies ist ein in sich geschlossener Client. Er reicht einen Batch von Render-Anfragen ein, begrenzt, wie viele Jobs gleichzeitig in Bearbeitung sind, fragt jeden Job in dem Takt ab, den der Server über Retry-After verlangt, meldet den vom Server gemeldeten progress, lädt jedes fertige PDF herunter und protokolliert Fehlschläge. Er nutzt einen PSR-18-HTTP-Client und PSR-17-Factories (den Transport-Vertrag, auf den sich die Connect-Recipes standardisieren) und fängt jeweils die spezifischste Exception ab, die jeder Aufruf auslösen kann: Psr\Http\Client\ClientExceptionInterface für einen Transportfehler und eine typisierte BatchJobException für eine Serverantwort, mit der der Batch nicht fortfahren kann. Kein catch-Block ist leer. Jeder protokolliert und löst die Exception erneut aus oder hält ein definiertes Ergebnis fest.
Ersetzen Sie die direkt im Code stehende $documents-Liste durch Ihre eigenen Eingaben. Injizieren Sie den konkreten HTTP-Client und die Factories Ihres Projekts dort, wo der Konstruktor die PSR-Interfaces erwartet.
<?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";}Erwartetes STDOUT, eine Zeile pro Dokument; die Pfade hängen von Ihrem Ausgabeverzeichnis ab:
invoice-0001 -> completed, written to /tmp/invoice-0001.pdfinvoice-0002 -> completed, written to /tmp/invoice-0002.pdfGrenzfälle & Stolperfallen
Abschnitt betitelt „Grenzfälle & Stolperfallen“- Lesen Sie die Job-Felder unter
data, nicht auf der obersten Ebene. Jede Erfolgsantwort ist in einen{ "data": ..., "meta": ... }-Umschlag verpackt.data.statusunddata.progresssind die Felder, auf die Sie reagieren;metaträgt dierequest_idfür die Support-Korrelation. progresskann fehlen. Der Server schließtprogressnur ein, wenn er ihn für diesen Job verfolgt. Behandeln Sie ein fehlendes Feld als „unbekannt“, nicht als null, und steuern Sie Ihre Schleife überstatus, der immer vorhanden ist.- Die Einreichung kann bereits terminal sein. Im aktuellen Release rendert der Server inline, bevor er den
POSTbeantwortet, sodass die Einreichungsantwortstatus: completedtragen kann und das Ergebnis schon bei der ersten Abfrage bereit sein kann. Ihre Poll-Schleife muss schon beim nullten Versuch einen Endzustand akzeptieren, statt zuerst auf einempendingzu bestehen. - Halten Sie sich an
Retry-After. Nicht-terminale Statusantworten setzenRetry-After(ein Intervall von 2 Sekunden). Schnelleres Abfragen verschwendet Anfragen und provoziert einen429. Begrenzen Sie den Wert auf ein vernünftiges Band, statt ihm blind zu vertrauen. /resultvor dem Abschluss ist ein409. Rufen Sie den Ergebnis-Endpunkt erst auf, nachdem die Statusabfragecompletedanzeigt. Ein409 Conflictbedeutet, dass der Job nicht fertig ist; es ist kein Transportfehler.- Idempotency-Key verhindert doppelte Arbeit. Eine erneut gesendete Einreichung mit demselben Schlüssel gibt den ursprünglichen Job zurück (
200statt201). Verwenden Sie einen stabilen Schlüssel pro Dokument, damit ein Netzwerk-Retry niemals einen zweiten Render startet. Ein wiederverwendeter Schlüssel mit einem anderen Body ist ein409-Konflikt. - Jobs sind besitzergebunden. Ein unter einem API-Schlüssel eingereichter Job ist für einen anderen unsichtbar; ein
GETüber Besitzergrenzen hinweg gibt404zurück, nicht403. Fragen Sie mit demselben Credential ab, mit dem Sie eingereicht haben. - Ein
failed-Job trägt eineerror-Nachricht. Lesen Siedata.errorbei einem terminalenfailed-Status und halten Sie ihn fest. Wiederholen Sie nicht blind.
Performance
Abschnitt betitelt „Performance“Die Kosten eines Batches sind die Summe der Renders plus dem Overhead durch das Abfragen. Zwei Hebel steuern die Clientseite. Erstens: Begrenzen Sie die Nebenläufigkeit. Das maxInFlight-Limit legt fest, wie viele Jobs gleichzeitig verfolgt werden, was die Zahl offener Anfragen und den Speicher des Clients unabhängig von der Batch-Größe konstant hält. Setzen Sie es passend zur Worker-Anzahl des Servers, nicht höher; mehr Jobs in Bearbeitung als Worker verlängern nur die Warteschlangenzeit jedes Jobs. Zweitens: Respektieren Sie das Poll-Intervall. Jede Abfrage ist ein günstiger Status-Read, aber eine enge Schleife vervielfacht das Anfragevolumen und löst die Ratenbegrenzung aus. Der 2-Sekunden-Retry-After des Servers ist der richtige Standard, und der Runner begrenzt auf ein Band von 1 bis 30 Sekunden, damit ein einzelner langsamer Job das Fenster weder durch Busy-Looping belasten noch blockieren kann.
Verarbeiten Sie sehr große Batches in Fenstern (der Runner nutzt array_chunk), statt alles im Voraus einzureichen. Das begrenzt sowohl den verfolgten Zustand des Clients als auch die Warteschlangentiefe des Servers, sodass ein fehlerhafter oder überdimensionierter Batch innerhalb eines Fensters fehlschlägt statt erst nach Tausenden von Einreichungen.
Sicherheitshinweise
Abschnitt betitelt „Sicherheitshinweise“- Halten Sie den Bearer-Token aus Logs und URLs heraus. Der API-Schlüssel reist ausschließlich im
Authorization-Header. Platzieren Sie ihn niemals in einem Query-String, einer Log-Zeile oder einem geschriebenen Artefakt. Der Runner protokolliert diejob_idund denstatus, niemals das Credential. - Leiten Sie Ausgabepfade aus servergesteuerten Schlüsseln ab. Der Runner baut jeden Ausgabepfad aus dem Dokumentschlüssel, den Ihr Code gewählt hat, verbunden mit einem festen Ausgabeverzeichnis, niemals aus einem Wert in einer Serverantwort. Interpolieren Sie kein Job-Feld in einen Dateisystempfad, da dies Path-Traversal ermöglichen würde.
- Validieren Sie die heruntergeladenen Bytes. Der Runner prüft bei einem
200von/resultden%PDF-Header, bevor er die Datei schreibt. Ein erfolgreicher Download-Status ist für sich allein noch kein Beweis, dass der Body ein PDF ist. - Behandeln Sie das Ergebnis als nicht vertrauenswürdig, bis es geprüft ist. Ein fertiger Job bedeutet, dass der Server Bytes gerendert hat, nicht, dass diese Bytes gefahrlos weitergeleitet werden können. Lassen Sie die Ergebnisse einen strukturellen Inspektionsschritt durchlaufen, bevor Sie sie an einen Client oder ein nachgelagertes System übergeben.
- Verwenden Sie einen Schlüssel nach dem Least-Privilege-Prinzip. Die Fläche für asynchrone Jobs ist Rendering der Core-Stufe. Stellen Sie dem Batch einen Schlüssel aus, der genau auf die Operationen beschränkt ist, die er braucht, und rotieren Sie ihn nach dem Zeitplan, den Ihre Secret-Management-Policy vorgibt.
- Begrenzen Sie das Poll-Budget.
maxPollsverhindert, dass ein hängender Job den Client für immer festhält. Der Batch hält das Timeout als Ergebnis fest, statt zu blockieren, was verhindert, dass ein einziger schlechter Job dem Rest den Dienst verweigert.
Konformität
Abschnitt betitelt „Konformität“Dieses Recipe erhebt keinen normativen Standards-Anspruch. Es konsumiert die REST-Endpunkte der asynchronen Jobs von NextPDF Connect (POST /api/v1/jobs, GET /api/v1/jobs/{id}, GET /api/v1/jobs/{id}/result) und liest die Felder des Job-Datensatzes, die der Server definiert (status, progress, error, result_url, poll_url). Die Prüfung des %PDF-Headers bei einem heruntergeladenen Ergebnis bestätigt nur, dass die Antwort mit der PDF-Markierung beginnt; sie ist keine Gültigkeits- oder Konformitätsfeststellung. Für eine Standards-Prüfung über eine Reihe von Dokumenten nutzen Sie das Enterprise-Batch-Compliance-Tool. Siehe Batch-Standards-Prüfung über Connect, eine andere Fläche als die hier behandelten Rendering-Jobs.
Siehe auch
Abschnitt betitelt „Siehe auch“- Hallo-Welt über Connect: der kleinste einzelne Render, bevor Sie in Batches arbeiten.
- Connect-Recipe-Konventionen: der Vertrag aus Transport, Authentifizierung und Konformität, den jedes Connect-Recipe teilt.
- Exception-bewusste Fehlerbehandlung über Connect: wie der Server Fehler meldet und wie ein Client reagieren sollte.
- Batch-Standards-Prüfung über Connect: die Enterprise-Compliance-Fläche, getrennt von diesen Render-Jobs.