Wsadowe generowanie PDF przez Connect ze śledzeniem postępu
W skrócie
Dział zatytułowany „W skrócie”Przetwórz listę dokumentów od początku do końca z jednego procesu klienta za pomocą NextPDF Connect — samodzielnej dystrybucji silnika jako usługi HTTP. Ten przepis wysyła każde żądanie renderowania do punktu końcowego zadań asynchronicznych POST /api/v1/jobs, odpytuje każde zadanie przez GET /api/v1/jobs/{id} aż do osiągnięcia stanu końcowego, odczytuje zgłaszane przez serwer pola status i progress dla każdego zadania oraz pobiera każdy ukończony plik PDF z GET /api/v1/jobs/{id}/result.
Cykl życia zadania jest stały i prosty. Zadanie ma stan pending, następnie running, a potem dokładnie jeden stan końcowy: completed, failed lub cancelled. Odpowiedź ze stanem zawiera liczbę całkowitą progress od 0 do 100, gdy serwer ją śledzi, a przy każdym odpytaniu zadania w stanie niekońcowym także nagłówek Retry-After, który wskazuje, kiedy wysłać kolejne żądanie. Każde wysłanie oznacz nagłówkiem Idempotency-Key, aby ponowienie zwracało to samo zadanie zamiast rozpoczynać drugie renderowanie.
Ten przepis pokazuje wariant na poziomie protokołu. Wywołuje REST-owy interfejs bezpośrednio i nie zakłada zestawu narzędzi programistycznych (SDK) dla konkretnego języka, więc ten sam przepływ można przenieść do dowolnego klienta HTTP.
Instalacja
Dział zatytułowany „Instalacja”Po stronie serwera użyj standardowej dystrybucji Connect:
composer require nextpdf/serverKlient PHP w przykładzie produkcyjnym poniżej używa klienta HTTP oraz fabryk wiadomości zgodnych z PSR-18 i PSR-17. Zainstaluj implementacje używane już w projekcie, na przykład:
composer require psr/http-client psr/http-factoryPrzegląd koncepcyjny
Dział zatytułowany „Przegląd koncepcyjny”Interfejs zadań asynchronicznych oddziela wysyłanie od pobierania. Nie utrzymujesz jednego długiego, otwartego połączenia HTTP dla każdego dokumentu. Zamiast tego wysyłasz zadanie, otrzymujesz identyfikator i odpytujesz lekki punkt końcowy stanu, aż zadanie się zakończy. Taki model ułatwia obsługę partii: klient śledzi N niezależnych zadań naraz bez N zablokowanych połączeń.
Przepływ obsługują trzy punkty końcowe:
POST /api/v1/jobsprzyjmuje to samo ciało żądania renderowania, co synchroniczny punkt końcowy/api/v1/render: polepage_size, poleorientationoraz uporządkowaną tablicęoperations. Zwraca201 Createddla nowego zadania albo200 OK, gdyIdempotency-Keypasuje do zadania, które już wysłano.GET /api/v1/jobs/{id}zwraca bieżący rekord zadania. Dla zadania w stanie niekońcowym ustawia też nagłówekRetry-After(serwer stosuje interwał 2-sekundowy) oraz polepoll_url. Respektuj ten nagłówek, zamiast odpytywać w ciasnej pętli.GET /api/v1/jobs/{id}/resultprzesyła strumieniowo gotowy plik PDF jakoapplication/pdf. Zwraca409 Conflict, jeśli zadanie nie osiągnęło jeszcze stanucompleted, więc wywołuj ten punkt dopiero wtedy, gdy odpytanie stanu potwierdzi stan końcowy.
Każda pomyślna odpowiedź ma tę samą kopertę: obiekt data z polami zadania oraz obiekt meta z polami request_id, timestamp, duration_ms oraz api_version. Pola zadania, które odczytujesz, znajdują się pod data: data.status, data.progress, data.job_id oraz, w przypadku ukończonego zadania, data.result_url.
Jedno zastrzeżenie dotyczące bieżącego wydania: serwer przetwarza wysłane zadanie w trybie wbudowanym, zanim odpowie na POST. W praktyce odpowiedź na wysłanie może już zawierać końcowy status, a wynik może być gotowy przy pierwszym odpytaniu. Udokumentowany tutaj kontrakt odpytywania i postępu jest stabilnym kształtem API. Serwer utrzymuje go bez zmian, gdy zaplecze przetwarzania przechodzi na kolejkowaną pulę procesów roboczych, więc klient z pętlą odpytywania jest poprawny dziś i pozostanie poprawny po tej zmianie. Napisz pętlę odpytywania. Nie zakładaj, że pierwsza odpowiedź jest niekońcowa ani że jest końcowa.
Powierzchnia API
Dział zatytułowany „Powierzchnia API”Dokument OpenAPI serwera oraz routing JobHandler definiują REST-ową powierzchnię zadań asynchronicznych Connect:
POST /api/v1/jobs: wyślij zadanie renderowania. Opcjonalny nagłówek żądaniaIdempotency-Key. Ciało jest żądaniem renderowania (operationsjest wymagane i musi zawierać co najmniej jedną operację). Odpowiedzi:201nowe,200idempotentne powtórzenie,422nieprawidłowe ciało,409konflikt idempotencji,429ograniczenie liczby żądań.GET /api/v1/jobs/{id}: odpytaj stan. Odpowiedź200z rekordem zadania; nagłówekRetry-Afterobecny, gdy stan jest niekońcowy;404, jeśli zadanie nie istnieje lub należy do innego klienta.GET /api/v1/jobs/{id}/result: pobierz plik PDF.200application/pdf, gdycompleted;409, gdy jeszcze nieukończone;404, jeśli zadanie jest nieznane.DELETE /api/v1/jobs/{id}: anuluj zadanie w staniependinglubrunningalbo usuń zadanie w staniecompleted(204).
Rekord zadania pod data zawiera następujące pola, dokładnie w postaci serializowanej przez serwer.
job_id: identyfikator (przedrostekjob_oraz 24 znaki szesnastkowe).status: jeden zpending,running,completed,failed,cancelled. Pierwsze dwa są niekońcowe; ostatnie trzy są końcowe.created_at, a po ustawieniu takżestarted_atorazcompleted_at: znaczniki czasu ISO-8601.progress: liczba całkowita od 0 do 100, obecna tylko wtedy, gdy serwer ją śledzi dla danego zadania; w przeciwnym razie nieobecna (traktuj jako nieznaną).error: ciąg z komunikatem, obecny tylko w zadaniu w staniefailed.result_url: obecne tylko dla zadania w staniecompleted; ścieżka do pobrania wyniku.poll_url: obecne tylko wtedy, gdy zadanie jest w stanie niekońcowym.
Uwierzytelnianie odbywa się tokenem okaziciela w nagłówku Authorization: Authorization: Bearer npk_live_{kid}_{secret}.
Przykład kodu — szybki start
Dział zatytułowany „Przykład kodu — szybki start”Przeprowadza jedno zadanie od początku do końca na poziomie protokołu, aby pokazać trzy wywołania i zwracane pola. Wysyła zadanie, odpytuje raz i pobiera wynik. Przykład produkcyjny poniżej dodaje pętlę wsadową, oczekiwanie według Retry-After oraz pełną obsługę błędów.
# 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.pdfPrzykład kodu — produkcja
Dział zatytułowany „Przykład kodu — produkcja”Ten samodzielny klient wysyła partię żądań renderowania, ogranicza liczbę zadań przetwarzanych jednocześnie, odpytuje każde zadanie w tempie narzuconym przez serwer nagłówkiem Retry-After, zgłasza wartość progress zwróconą przez serwer, pobiera każdy ukończony plik PDF i rejestruje niepowodzenia. Używa klienta HTTP zgodnego z PSR-18 oraz fabryk PSR-17, czyli kontraktu transportu ujednoliconego w przepisach Connect. Przechwytuje także najbardziej szczegółowy wyjątek, jaki może zgłosić każde wywołanie: Psr\Http\Client\ClientExceptionInterface dla niepowodzenia transportu oraz typowany BatchJobException dla odpowiedzi serwera, która uniemożliwia kontynuację partii. Żaden blok catch nie jest pusty. Każdy z nich rejestruje i ponownie zgłasza wyjątek albo zapisuje zdefiniowany wynik.
Zastąp osadzoną listę $documents własnymi danymi wejściowymi. Wstrzyknij konkretnego klienta HTTP i fabryki z projektu tam, gdzie konstruktor oczekuje interfejsów 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";}Oczekiwane wyjście STDOUT zawiera jeden wiersz dla każdego dokumentu. Ścieżki zależą od katalogu wyjściowego:
invoice-0001 -> completed, written to /tmp/invoice-0001.pdfinvoice-0002 -> completed, written to /tmp/invoice-0002.pdfPrzypadki brzegowe i pułapki
Dział zatytułowany „Przypadki brzegowe i pułapki”- Odczytuj pola zadania pod
data, a nie na najwyższym poziomie. Każda pomyślna odpowiedź jest opakowana w kopertę{ "data": ..., "meta": ... }.data.statusidata.progressto pola, na których działasz;metazawierarequest_iddo korelacji w obsłudze zgłoszeń. - Pole
progressmoże być nieobecne. Serwer dołączaprogresstylko wtedy, gdy śledzi je dla danego zadania. Traktuj brakujące pole jako „nieznane”, a nie jako zero, i opieraj pętlę na polustatus, które jest zawsze obecne. - Odpowiedź na wysłanie może już wskazywać stan końcowy. W bieżącym wydaniu serwer renderuje w trybie wbudowanym, zanim odpowie na
POST, więc odpowiedź na wysłanie może zawieraćstatus: completed, a wynik może być gotowy przy pierwszym odpytaniu. Pętla odpytywania musi akceptować stan końcowy już przy próbie zerowej, zamiast wymagać najpierw stanupending. - Respektuj
Retry-After. Odpowiedzi ze stanem niekońcowym ustawiająRetry-After(interwał 2-sekundowy). Szybsze odpytywanie marnuje żądania i prowokuje429. Ogranicz wartość do rozsądnego zakresu, zamiast ufać jej bezkrytycznie. - Wywołanie
/resultprzed ukończeniem to409. Wywołuj punkt końcowy wyniku dopiero wtedy, gdy odpytanie stanu pokażecompleted.409 Conflictoznacza, że zadanie nie jest ukończone; nie jest to błąd transportu. - Idempotency-Key zapobiega zduplikowanej pracy. Ponowione wysłanie z tym samym kluczem zwraca pierwotne zadanie (
200zamiast201). Używaj stabilnego klucza dla każdego dokumentu, aby ponowienie sieciowe nigdy nie rozpoczynało drugiego renderowania. Ponowne użycie klucza z innym ciałem powoduje konflikt409. - Zadania są ograniczone do właściciela. Zadanie wysłane z użyciem jednego klucza API jest niewidoczne dla innego;
GETmiędzy właścicielami zwraca404, a nie403. Odpytuj przy użyciu tego samego poświadczenia, którym wysłano zadanie. - Zadanie w stanie
failedzawiera komunikaterror. Przy końcowym staniefailedodczytajdata.errori zapisz go. Nie ponawiaj bezkrytycznie.
Wydajność
Dział zatytułowany „Wydajność”Koszt partii to suma renderowań plus narzut odpytywania. Po stronie klienta liczą się dwa mechanizmy. Po pierwsze, ogranicz współbieżność: limit maxInFlight ustala, ile zadań jest śledzonych naraz, co utrzymuje liczbę otwartych żądań i zużycie pamięci klienta na stałym poziomie niezależnie od rozmiaru partii. Ustaw go tak, aby odpowiadał liczbie procesów roboczych serwera, nie wyżej; liczba zadań przetwarzanych jednocześnie większa niż liczba procesów roboczych tylko wydłuża czas oczekiwania każdego zadania w kolejce. Po drugie, respektuj interwał odpytywania: każde odpytanie to tani odczyt stanu, ale ciasna pętla zwiększa liczbę żądań i wyzwala mechanizm ograniczania liczby żądań. 2-sekundowa wartość Retry-After serwera to właściwa wartość domyślna, a runner ogranicza ją do zakresu od 1 do 30 sekund, aby pojedyncze wolne zadanie nie mogło wymusić jałowej pętli ani zablokować okna.
W przypadku bardzo dużych partii przetwarzaj dokumenty w oknach (runner używa array_chunk), zamiast wysyłać wszystko z góry. Ogranicza to zarówno śledzony stan klienta, jak i głębokość kolejki serwera, więc wadliwa lub zbyt duża partia kończy się niepowodzeniem wewnątrz jednego okna, a nie po tysiącach wysłań.
Uwagi dotyczące bezpieczeństwa
Dział zatytułowany „Uwagi dotyczące bezpieczeństwa”- Trzymaj token okaziciela z dala od logów i adresów URL. Klucz API jest przesyłany wyłącznie w nagłówku
Authorization. Nigdy nie umieszczaj go w ciągu zapytania, w wierszu logu ani w zapisanym artefakcie. Runner logujejob_idorazstatus, nigdy poświadczenia. - Wyprowadzaj ścieżki wyjściowe z kluczy kontrolowanych po stronie serwera. Runner buduje każdą ścieżkę wyjściową z klucza dokumentu wybranego przez Twój kod, połączonego ze stałym katalogiem wyjściowym, nigdy z wartości w odpowiedzi serwera. Nie wstawiaj pola zadania do ścieżki w systemie plików, ponieważ umożliwiłoby to atak typu path traversal.
- Weryfikuj pobrane bajty. Runner sprawdza, czy odpowiedź
200z/resultzaczyna się od nagłówka%PDF, zanim zapisze plik. Sam pomyślny status pobrania nie dowodzi, że ciało jest plikiem PDF. - Traktuj wynik jako niezaufany do czasu sprawdzenia. Ukończone zadanie oznacza, że serwer wyrenderował bajty, a nie że można je bezpiecznie przekazać dalej. Przepuść wyniki przez krok inspekcji strukturalnej, zanim przekażesz je klientowi lub dalszemu systemowi.
- Używaj klucza o najmniejszych uprawnieniach. Interfejs zadań asynchronicznych to renderowanie warstwy core. Dla partii wystaw klucz o zakresie obejmującym dokładnie te operacje, których potrzebuje, i rotuj go zgodnie z harmonogramem ustalonym przez politykę zarządzania kluczami tajnymi.
- Ogranicz budżet odpytywania.
maxPollspowstrzymuje zablokowane zadanie przed utrzymywaniem klienta w nieskończoność. Partia rejestruje przekroczenie limitu czasu jako wynik zamiast blokować się, co zapobiega odmowie usługi pozostałym zadaniom przez jedno wadliwe zadanie.
Zgodność
Dział zatytułowany „Zgodność”Ten przepis nie formułuje żadnych normatywnych twierdzeń o zgodności ze standardami. Korzysta z REST-owych punktów końcowych zadań asynchronicznych NextPDF Connect (POST /api/v1/jobs, GET /api/v1/jobs/{id}, GET /api/v1/jobs/{id}/result) i odczytuje pola rekordu zadania, które definiuje serwer (status, progress, error, result_url, poll_url). Sprawdzenie nagłówka %PDF w pobranym wyniku potwierdza tylko, że odpowiedź zaczyna się znacznikiem PDF; nie rozstrzyga o ważności ani zgodności. Do sprawdzania zgodności ze standardami w partii dokumentów użyj narzędzia do wsadowej kontroli zgodności w edycji Enterprise. Zobacz Wsadowa kontrola standardów przez Connect, czyli inny interfejs niż omawiane tutaj zadania renderowania.
Zobacz także
Dział zatytułowany „Zobacz także”- Hello world przez Connect: najmniejsze pojedyncze renderowanie przed rozpoczęciem przetwarzania wsadowego.
- Konwencje przepisów Connect: kontrakt transportu, uwierzytelniania i zgodności wspólny dla każdego przepisu Connect.
- Obsługa błędów świadoma wyjątków przez Connect: jak serwer zgłasza błędy i jak klient powinien na nie reagować.
- Wsadowa kontrola standardów przez Connect: interfejs zgodności w edycji Enterprise, odrębny od omawianych tutaj zadań renderowania.