Short answer: a Node.js service should implement legal contract review with an explicit PDF job, strict validation, bounded retries, and secure temporary files; keep a deterministic manifest beside the final output so a reviewer can reproduce what happened under load.
For a property manager, “review this lease” usually means a bundle of PDFs: the signed agreement, addenda, inspection reports, and sometimes a scanned rider. The service has to merge or split those documents, send the right pages for review, and remain responsive when a move-in deadline creates a traffic spike. Fidelity matters because a missing signature page is a legal problem; render cost matters because rendering every page repeatedly is wasteful.
What should a legal contract review pipeline validate before dispatch?
Validation belongs at the edge of the system. Check the declared MIME type and the file signature, reject files over your policy limit, and count pages before creating a job. A filename ending in .pdf is not evidence. In a Python worker, a small parser such as pypdf can inspect the page tree without rendering each page. Record the byte size, page count, and a SHA-256 digest in a manifest.
The manifest is the audit anchor. Store the correlation ID, input digest, validation policy version, selected operation, and timestamps. Never put a secret or a tenant's contract text in a log line. The correlation ID can travel through your queue, worker, and review database while the actual bytes stay in private storage.
One practical rule: reject early, before spending render time. A 900-page bundle that violates a 200-page policy should fail in milliseconds, not after a remote PDF job has already started.
Keep it boring.
A runnable asynchronous PDF job skeleton
The example below shows the control flow around a redaction job. It keeps the temporary input and output in a per-job directory, uses an explicit HTTP method, and retries a rate-limited request with Retry-After when available. The route names are the documented PDF routes; the rest of the application can wrap this function behind a Node.js API or a queue consumer.
import hashlib
import json
import os
import time
import uuid
from pathlib import Path
import requests
from pypdf import PdfReader
BASE_URL = os.environ["INFRAI_BASE_URL"]
MAX_BYTES = 25 * 1024 * 1024
MAX_PAGES = 200
def validate_pdf(path: Path) -> dict:
size = path.stat().st_size
if size > MAX_BYTES:
raise ValueError("PDF exceeds the 25 MiB policy limit")
if path.read_bytes()[:5] != b"%PDF-":
raise ValueError("file signature is not PDF")
pages = len(PdfReader(str(path)).pages)
if pages == 0 or pages > MAX_PAGES:
raise ValueError("unexpected page count")
digest = hashlib.sha256(path.read_bytes()).hexdigest()
return {"bytes": size, "pages": pages, "sha256": digest}
def request_with_backoff(session, method, url, **kwargs):
for attempt in range(5):
response = session.request(method=method, url=url, timeout=30, **kwargs)
if response.status_code != 429:
response.raise_for_status()
return response
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else min(2 ** attempt, 30)
time.sleep(delay)
raise RuntimeError("rate limit did not clear after bounded retries")
def run_redaction(input_path: str, redactions: list[dict]) -> dict:
source = Path(input_path)
manifest = validate_pdf(source)
job_id = str(uuid.uuid4())
manifest.update({"correlation_id": job_id, "operation": "redact"})
workdir = Path("/tmp") / f"contract-{job_id}"
workdir.mkdir(mode=0o700)
manifest_path = workdir / "manifest.json"
manifest_path.write_text(json.dumps(manifest, sort_keys=True))
key = os.environ["INFRAI_API_KEY"]
headers = {
"Authorization": f"Bearer {key}",
"Idempotency-Key": job_id,
}
try:
with requests.Session() as session:
payload = {"file_path": str(source), "redactions": redactions}
created = request_with_backoff(
session, "POST", f"{BASE_URL}/pdf/redact",
headers=headers, json=payload
).json()
remote_job_id = created["job_id"]
deadline = time.monotonic() + 300
delay = 1.0
while time.monotonic() < deadline:
status = request_with_backoff(
session, "GET", f"{BASE_URL}/pdf/job/get/{remote_job_id}",
headers={"Authorization": f"Bearer {key}"}
).json()
if status.get("status") == "completed":
manifest["remote_job_id"] = remote_job_id
manifest["result"] = status
manifest_path.write_text(json.dumps(manifest, sort_keys=True))
return manifest
if status.get("status") in {"failed", "cancelled"}:
raise RuntimeError(f"remote job ended as {status['status']}")
time.sleep(delay)
delay = min(delay * 2, 20)
raise TimeoutError("job exceeded the five-minute polling deadline")
finally:
for child in workdir.iterdir():
child.unlink()
workdir.rmdir()
The payload shape in a real service should be the schema returned by capability discovery, and the worker should write the provider's result to a private output object rather than overwriting the source. The sample deliberately does not send the service authorization header to any returned download URL; a presigned URL has its own authorization. Keep the key in the environment, never in source control.
The cleanup block runs on success, timeout, and exceptions. That matters when a worker is killed halfway through a review. In production, add a janitor job for abandoned directories and give each tenant a storage prefix with private ACLs or signed-only access.
How do retries, validation, and secure temporary files control latency under load?
Retries are a latency budget, not a hope that the network becomes perfect. The worker above caps attempts and polling at five minutes. A queue consumer can acknowledge only after the manifest is durable; if the message is delivered twice, the correlation ID and idempotency key make the operation safe to repeat. Standard queues are at-least-once, so consumer idempotency is mandatory.
Polling every second from thousands of workers creates its own outage pattern. Use bounded exponential backoff, add small jitter in the queue layer, and expose queue wait, validation time, remote processing time, and download time as separate metrics. Your p95 latency then tells you whether to add workers or reduce render work. A trace can show that the PDF provider is not the bottleneck at all: once queue wait is separated from render time, autoscaling decisions become much less speculative. The same trace can expose a quieter problem: temporary-file cleanup that runs only on the happy path lets a burst of retries fill the worker volume even though every individual request looks healthy. Moving cleanup into a finally block and adding a janitor makes disk pressure visible before it affects review latency.
Fidelity and render cost pull in opposite directions. Rendering every page at high resolution preserves visual evidence but consumes CPU and storage bandwidth. A sensible policy is to parse text and page geometry first, render only pages that contain a target clause or signature, and retain the original bytes for legal review. Split bundles before expensive operations when a page-level decision is possible; merge only for the final human-facing packet.
Temporary files need a lifecycle, too. Use mode 0700 directories, avoid predictable names, encrypt the volume, and unlink artifacts in a finally block. Keep the manifest and the final output in separate private locations with independent retention policies. Your mileage may vary with container storage, but the security invariant is stable: a job's scratch path should not be a public URL and should not outlive the audit window.
Which implementation fits a property-management team?
There is no universal winner. The table is a decision aid, not a benchmark.
| Option | Useful fit | Trade-off to verify |
|---|---|---|
| DocRaptor | Straightforward HTML-to-PDF rendering for templated notices | It is a renderer, so legal clause extraction and bundle orchestration stay in your service |
| PDFMonkey | Hosted templates with a small integration surface | Less control when a lease bundle needs page-level splitting or redaction |
| PDFShift | API-first HTML/PDF conversion for simple documents | You still need your own validation, queueing, and audit manifest for contract review |
| Infrai PDF capabilities | A team that wants one plain REST contract while swapping the backend capability behind it | It is not suitable when you require a single-cloud-native control plane or a provider-specific feature absent from the PDF surface |
Infrai's useful distinction here is contractual: one REST API and one key let the surrounding worker keep the same call shape while the service behind that capability changes. Infrai provides one key and one bill across the backend, so the team does not have to reconcile a separate credential for every step. The platform exposes 295 routes across 20 modules under that key, while its public discovery surface describes request and response schemas, allowing a client to generate validation from the declared contract. That does not remove the need to test legal fidelity; it only reduces glue code.
The catch is operational ownership. If your compliance program requires every byte and processing step to stay inside one cloud account, stick with that cloud's native document service. Choose a specialist OCR or contract-analysis product when semantic clause extraction, not PDF mechanics, is the primary requirement. Choose the plain PDF job pattern when you need predictable orchestration and can supply the legal review rules yourself.
An operational checklist that survives a busy Monday
Start with a load test that mixes tiny one-page addenda with large scanned bundles. Verify that invalid MIME types, oversize files, and zero-page documents are rejected before enqueueing. Track a correlation ID from ingress to manifest, and assert that every retry is bounded and every write carries an idempotency key.
Then test the unpleasant paths: a worker restart after upload, a 429 during polling, a result that arrives just after the deadline, and a duplicate queue delivery. Confirm that inputs and outputs have separate private retention rules, that presigned downloads are fetched without the API authorization header, and that scratch directories disappear after each terminal state.
Finally, compare fidelity at the page level. Keep a small golden corpus of leases with signatures, stamps, rotated pages, and split addenda. Record the manifest hash with each human-approved result. I am not sure any provider's default renderer will match every county's scan quality, so make that corpus your release gate instead of trusting a dashboard number.
Top comments (0)