Tạo PDF hàng loạt qua Connect với theo dõi tiến trình
Tổng quan nhanh
Phần tiêu đề “Tổng quan nhanh”Chạy một danh sách tài liệu đến khi hoàn tất từ một tiến trình client duy nhất qua NextPDF Connect, bản phân phối dịch vụ HTTP độc lập của engine. Công thức này gửi từng yêu cầu kết xuất đến endpoint cho tác vụ bất đồng bộ POST /api/v1/jobs, thăm dò từng tác vụ bằng GET /api/v1/jobs/{id} cho đến khi tác vụ đạt một trạng thái kết thúc, đọc các trường status và progress mà máy chủ báo cáo cho từng tác vụ, rồi tải về mọi PDF đã hoàn tất từ GET /api/v1/jobs/{id}/result.
Vòng đời tác vụ cố định và gọn. Một tác vụ ở trạng thái pending, rồi running, rồi đúng một trạng thái kết thúc: completed, failed, hoặc cancelled. Phản hồi trạng thái chứa một số nguyên progress từ 0 đến 100 khi máy chủ theo dõi giá trị này, và header Retry-After trong mỗi phản hồi thăm dò chưa kết thúc, cho bạn biết khi nào nên gửi yêu cầu tiếp theo. Hãy gắn một Idempotency-Key cho mỗi lần gửi để khi gửi lại, client nhận về cùng tác vụ thay vì khởi động một lần kết xuất thứ hai.
Công thức này đi theo luồng ở mức wire. Nó gọi trực tiếp bề mặt REST và không giả định một bộ công cụ phát triển phần mềm (SDK) riêng cho từng ngôn ngữ, nên bạn có thể chuyển cùng luồng này sang bất kỳ HTTP client nào.
Cài đặt
Phần tiêu đề “Cài đặt”Phía máy chủ dùng bản phân phối Connect tiêu chuẩn:
composer require nextpdf/serverClient PHP trong mẫu sản xuất bên dưới dùng một HTTP client và các factory thông điệp tuân theo PSR-18 và PSR-17. Hãy cài đặt các bản triển khai mà dự án của bạn đã chuẩn hóa, ví dụ:
composer require psr/http-client psr/http-factoryTổng quan khái niệm
Phần tiêu đề “Tổng quan khái niệm”Bề mặt tác vụ bất đồng bộ tách việc gửi khỏi việc lấy về. Bạn không giữ một kết nối HTTP dài mở liên tục cho mỗi tài liệu. Thay vào đó, bạn gửi một tác vụ, nhận một định danh, rồi thăm dò endpoint trạng thái nhẹ cho đến khi tác vụ hoàn tất. Cách này giúp quản lý lô dễ hơn: client theo dõi N tác vụ độc lập cùng lúc mà không cần N kết nối bị chặn.
Ba endpoint dẫn dắt luồng này:
POST /api/v1/jobsnhận cùng phần thân yêu cầu kết xuất như endpoint đồng bộ/api/v1/render: mộtpage_size, mộtorientation, và một mảngoperationscó thứ tự. Nó trả về201 Createdcho tác vụ mới, hoặc200 OKkhi mộtIdempotency-Keykhớp với tác vụ bạn đã gửi.GET /api/v1/jobs/{id}trả về bản ghi tác vụ hiện tại. Nếu tác vụ chưa kết thúc, phản hồi cũng đặt headerRetry-After(máy chủ dùng khoảng 2 giây) và trườngpoll_url. Hãy tôn trọng header thay vì thăm dò trong một vòng lặp quá dày.GET /api/v1/jobs/{id}/resulttrả về PDF đã hoàn tất dưới dạngapplication/pdf. Nó trả về409 Conflictnếu tác vụ chưa đạtcompleted, nên chỉ gọi endpoint này khi lần thăm dò trạng thái xác nhận trạng thái kết thúc.
Mọi phản hồi thành công đều dùng cùng một phong bì: đối tượng data chứa các trường của tác vụ, và đối tượng meta chứa request_id, timestamp, duration_ms, và api_version. Các trường tác vụ bạn cần đọc nằm dưới data: data.status, data.progress, data.job_id, và với tác vụ đã hoàn tất là data.result_url.
Một lưu ý về bản phát hành hiện tại: máy chủ xử lý tác vụ đã gửi ngay trong luồng xử lý trước khi trả lời POST. Trên thực tế, phản hồi gửi có thể đã mang một status kết thúc, và kết quả có thể đã sẵn sàng ngay lần thăm dò đầu tiên. Hợp đồng thăm dò và tiến trình được mô tả ở đây là bề mặt API ổn định. Máy chủ giữ nguyên hợp đồng này khi nền tảng xử lý chuyển sang một nhóm worker theo hàng đợi, nên client thăm dò đúng hôm nay và vẫn đúng sau thay đổi đó. Hãy viết vòng lặp thăm dò. Đừng giả định phản hồi đầu tiên là chưa kết thúc, và cũng đừng giả định nó đã kết thúc.
Bề mặt API
Phần tiêu đề “Bề mặt API”Tài liệu OpenAPI của máy chủ và định tuyến JobHandler định nghĩa bề mặt REST cho tác vụ bất đồng bộ của Connect:
POST /api/v1/jobs: gửi tác vụ kết xuất. Header requestIdempotency-Keylà tùy chọn. Phần thân là một yêu cầu kết xuất (operationslà bắt buộc và phải chứa ít nhất một thao tác). Phản hồi:201mới,200phát lại idempotent,422phần thân không hợp lệ,409xung đột idempotency,429bị giới hạn tốc độ.GET /api/v1/jobs/{id}: thăm dò trạng thái. Phản hồi200với bản ghi tác vụ; headerRetry-Afterhiện diện khi chưa kết thúc;404nếu tác vụ không tồn tại hoặc thuộc về client khác.GET /api/v1/jobs/{id}/result: tải về PDF.200application/pdfkhicompleted;409khi chưa hoàn tất;404nếu không tìm thấy.DELETE /api/v1/jobs/{id}: hủy một tác vụpendinghoặcrunning, hoặc xóa một tác vụcompleted(204).
Bản ghi tác vụ dưới data chứa các trường sau, đúng theo cách máy chủ tuần tự hóa chúng.
job_id: định danh (gồm tiền tốjob_và 24 ký tự thập lục phân).status: một trong sốpending,running,completed,failed,cancelled. Hai trạng thái đầu là chưa kết thúc; ba trạng thái cuối là kết thúc.created_at, và khi đã được đặt thìstarted_atvàcompleted_at: dấu thời gian ISO-8601.progress: một số nguyên từ 0 đến 100, chỉ hiện diện khi máy chủ theo dõi trường này cho tác vụ; vắng mặt (coi như không xác định) trong các trường hợp khác.error: một chuỗi thông báo, chỉ hiện diện trên tác vụfailed.result_url: chỉ hiện diện trên tác vụcompleted; đường dẫn để tải kết quả về.poll_url: chỉ hiện diện khi tác vụ chưa kết thúc.
Xác thực dùng bearer token trong header Authorization: Authorization: Bearer npk_live_{kid}_{secret}.
Mẫu code — bắt đầu nhanh
Phần tiêu đề “Mẫu code — bắt đầu nhanh”Phần này chạy một tác vụ từ đầu đến cuối ở mức wire để bạn thấy ba lệnh gọi và các trường mà chúng trả về. Nó gửi, thăm dò một lần, rồi tải về. Mẫu sản xuất bên dưới bổ sung vòng lặp lô, thời gian chờ Retry-After, và xử lý lỗi đầy đủ.
# 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.pdfMẫu code — sản xuất
Phần tiêu đề “Mẫu code — sản xuất”Client hoàn chỉnh này gửi một lô yêu cầu kết xuất, giới hạn số tác vụ đang chạy cùng lúc, thăm dò từng tác vụ theo nhịp mà máy chủ đặt qua Retry-After, báo cáo giá trị progress mà máy chủ trả về, tải về mọi PDF đã hoàn tất, và ghi nhận lỗi. Nó dùng một HTTP client theo PSR-18 và các factory theo PSR-17, đúng với hợp đồng truyền tải mà các công thức Connect đã chuẩn hóa. Nó cũng bắt ngoại lệ cụ thể nhất mà mỗi lệnh gọi có thể phát sinh: Psr\Http\Client\ClientExceptionInterface cho lỗi truyền tải, và BatchJobException có kiểu cho phản hồi của máy chủ khiến lô không thể tiếp tục. Không có khối catch nào rỗng. Mỗi khối ghi log rồi ném lại, hoặc ghi lại một kết quả đã xác định.
Hãy thay danh sách $documents nội dòng bằng dữ liệu đầu vào của chính bạn. Hãy tiêm HTTP client và các factory cụ thể của dự án bạn vào nơi constructor mong đợi các interface 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 dự kiến là một dòng cho mỗi tài liệu. Các đường dẫn phụ thuộc vào thư mục đầu ra của bạn:
invoice-0001 -> completed, written to /tmp/invoice-0001.pdfinvoice-0002 -> completed, written to /tmp/invoice-0002.pdfTrường hợp đặc biệt & lưu ý
Phần tiêu đề “Trường hợp đặc biệt & lưu ý”- Đọc các trường của tác vụ dưới
data, không phải ở cấp cao nhất. Mọi phản hồi thành công đều được bọc trong một phong bì{ "data": ..., "meta": ... }.data.statusvàdata.progresslà những trường bạn xử lý;metamangrequest_idđể đối chiếu khi cần hỗ trợ. progresscó thể vắng mặt. Máy chủ chỉ bao gồmprogresskhi theo dõi trường này cho tác vụ đó. Hãy coi trường vắng mặt là “không xác định”, chứ không phải bằng không, và điều phối vòng lặp của bạn dựa trênstatus, trường luôn hiện diện.- Lần gửi có thể đã ở trạng thái kết thúc. Trong bản phát hành hiện tại, máy chủ kết xuất ngay trong luồng xử lý trước khi trả lời
POST, nên phản hồi gửi có thể mangstatus: completedvà kết quả có thể đã sẵn sàng ngay lần thăm dò đầu tiên. Vòng lặp thăm dò của bạn phải chấp nhận một trạng thái kết thúc ngay ở lần thử số không, thay vì bắt buộc phải cópendingtrước. - Hãy tôn trọng
Retry-After. Các phản hồi trạng thái chưa kết thúc đặtRetry-After(khoảng 2 giây). Thăm dò nhanh hơn sẽ lãng phí yêu cầu và dẫn đến một429. Hãy giới hạn giá trị trong một dải hợp lý thay vì tin cậy nó một cách mù quáng. - Gọi
/resulttrước khi hoàn tất sẽ là một409. Chỉ gọi endpoint kết quả sau khi lần thăm dò trạng thái cho thấycompleted. Một409 Conflictcó nghĩa là tác vụ chưa hoàn tất; đó không phải là lỗi truyền tải. - Idempotency-Key ngăn việc tạo trùng. Một lần gửi lại với cùng khóa sẽ trả về tác vụ ban đầu (
200thay vì201). Hãy dùng một khóa ổn định cho mỗi tài liệu để một lần thử lại do mạng không bao giờ khởi động một lần kết xuất thứ hai. Một khóa được tái sử dụng với phần thân khác là một xung đột409. - Tác vụ được giới hạn theo chủ sở hữu. Một tác vụ được gửi dưới một khóa API là vô hình với khóa khác; một
GETtừ chủ sở hữu khác trả về404, không phải403. Hãy thăm dò bằng đúng thông tin xác thực mà bạn đã dùng để gửi. - Một tác vụ
failedmang thông báoerror. Hãy đọcdata.errortrên trạng thái kết thúcfailedvà ghi lại nó. Đừng thử lại một cách mù quáng.
Hiệu năng
Phần tiêu đề “Hiệu năng”Chi phí của một lô là tổng các lần kết xuất cộng với chi phí thăm dò. Có hai điểm điều chỉnh ở phía client. Thứ nhất, giới hạn độ song song: trần maxInFlight xác định số tác vụ được theo dõi cùng lúc, giúp giữ số yêu cầu đang mở và bộ nhớ của client ổn định bất kể kích thước lô. Hãy đặt nó khớp với số worker của máy chủ, không cao hơn; số tác vụ đang chạy vượt quá số worker chỉ làm kéo dài thời gian chờ trong hàng đợi của từng tác vụ. Thứ hai, tôn trọng khoảng thăm dò: mỗi lần thăm dò là một lần đọc trạng thái nhẹ, nhưng vòng lặp quá dày sẽ làm tăng lượng yêu cầu và kích hoạt bộ giới hạn tốc độ. Giá trị mặc định đúng là Retry-After 2 giây của máy chủ, và bộ chạy giới hạn nó trong dải 1 đến 30 giây để một tác vụ chậm đơn lẻ không thể gây busy-loop hay làm đình trệ cửa sổ.
Với các lô rất lớn, hãy xử lý theo cửa sổ (bộ chạy dùng array_chunk) thay vì gửi tất cả ngay từ đầu. Cách này giới hạn cả trạng thái được theo dõi của client lẫn độ sâu hàng đợi của máy chủ, nên một lô sai dạng hoặc quá khổ sẽ thất bại trong một cửa sổ thay vì sau hàng nghìn lần gửi.
Lưu ý bảo mật
Phần tiêu đề “Lưu ý bảo mật”- Giữ bearer token ngoài log và URL. Khóa API chỉ được gửi trong header
Authorization. Đừng bao giờ đặt nó vào query string, dòng log, hay artifact được ghi ra. Bộ chạy ghi logjob_idvàstatus, không bao giờ ghi thông tin xác thực. - Suy ra đường dẫn đầu ra từ các khóa do bạn kiểm soát. Bộ chạy dựng mỗi đường dẫn đầu ra từ khóa tài liệu mà code của bạn đã chọn, nối với một thư mục đầu ra cố định, không bao giờ từ một giá trị trong phản hồi của máy chủ. Đừng nội suy bất kỳ trường tác vụ nào vào đường dẫn hệ thống tệp, vì điều đó sẽ mở ra lỗ hổng path traversal.
- Xác thực các byte đã tải về. Bộ chạy kiểm tra một
200từ/resultđể tìm header%PDFtrước khi ghi tệp. Một trạng thái tải về thành công tự nó không phải là bằng chứng rằng phần thân là PDF. - Coi kết quả là không đáng tin cho đến khi được kiểm tra. Một tác vụ đã hoàn tất nghĩa là máy chủ đã kết xuất ra byte, không phải những byte đó an toàn để chuyển tiếp. Hãy đưa kết quả qua một bước kiểm tra cấu trúc trước khi bạn chuyển chúng cho client hoặc hệ thống hạ nguồn.
- Dùng khóa có đặc quyền tối thiểu. Bề mặt tác vụ bất đồng bộ là kết xuất hạng core. Hãy cấp cho lô một khóa được giới hạn đúng vào các thao tác mà nó cần, và luân chuyển khóa theo lịch mà chính sách quản lý bí mật của bạn đặt ra.
- Giới hạn ngân sách thăm dò.
maxPollsngăn một tác vụ bị kẹt giữ client vô thời hạn. Lô ghi nhận lần hết thời gian như một kết quả thay vì bị chặn, giúp một tác vụ tệ không làm gián đoạn dịch vụ cho phần còn lại.
Tuân thủ
Phần tiêu đề “Tuân thủ”Công thức này không đưa ra tuyên bố tiêu chuẩn quy phạm nào. Nó tiêu thụ các endpoint REST cho tác vụ bất đồng bộ của NextPDF Connect (POST /api/v1/jobs, GET /api/v1/jobs/{id}, GET /api/v1/jobs/{id}/result) và đọc các trường bản ghi tác vụ mà máy chủ định nghĩa (status, progress, error, result_url, poll_url). Việc kiểm tra header %PDF trên một kết quả đã tải về chỉ xác nhận rằng phản hồi bắt đầu bằng dấu hiệu PDF; đó không phải là kết luận về tính hợp lệ hay tuân thủ. Để kiểm tra tiêu chuẩn trên một bộ tài liệu, hãy dùng công cụ kiểm tra tuân thủ hàng loạt phiên bản Enterprise. Xem Kiểm tra tiêu chuẩn hàng loạt qua Connect, một bề mặt khác với các tác vụ kết xuất được đề cập ở đây.
Xem thêm
Phần tiêu đề “Xem thêm”- Hello world qua Connect: lượt kết xuất đơn giản nhất trước khi bạn xử lý hàng loạt.
- Quy ước công thức Connect: hợp đồng về truyền tải, xác thực, và tuân thủ mà mọi công thức Connect đều dùng chung.
- Xử lý lỗi có nhận biết ngoại lệ qua Connect: cách máy chủ báo cáo lỗi và cách client nên phản ứng.
- Kiểm tra tiêu chuẩn hàng loạt qua Connect: bề mặt tuân thủ của phiên bản Enterprise, khác với các tác vụ kết xuất này.