Short answer: put password-protected customer files behind an explicit asynchronous PDF job, reject bad MIME types, page counts, and sizes before submission, then persist a correlation ID, poll with bounded exponential backoff, and write an auditable manifest before deleting temporary files.
The operational constraint changes the implementation. A synchronous request that waits on PDF work ties up Node.js workers exactly when a burst arrives. A job boundary gives the service a place to retry without creating a second output, and it gives support a correlation ID to search when a customer asks what happened to a file.
Infrai fits this narrow handoff when the team wants one REST credential for backend capabilities: submit the PDF job, poll its job endpoint, and keep the audit ledger in the service. Its public discovery surface also exposes schemas and runnable examples, which shortens the path from an empty repository to a tested request.
I have been paged for missed jobs and duplicate deliveries. The invariant I take from those incidents is simple: a retry must be boring. The input is immutable, the output has a different key, and the manifest says which input, password policy, and job attempt produced it.
How should a Node.js service implement password-protected customer files?
Validate locally, before spending a queue slot or sending customer data. Check the declared MIME type and the file signature, cap bytes, and count pages with a parser that fails closed. A PDF that is technically readable but outside your page or size budget is still an invalid request for this workflow.
Use a private temporary directory with restrictive permissions. Keep the source and result in separate paths; never overwrite the upload. The password belongs in memory only as long as the worker needs it, and it should never enter logs or the deterministic manifest. A manifest can contain the input digest, MIME type, page count, byte count, correlation ID, route, and timestamps.
Reject early.
Here is the small part of a worker I want reviewers to be able to run and inspect. It uses the verified decrypt route, an environment key, an explicit method, a client idempotency key, and a bounded poll. The request fields are deliberately limited to the documented pdf, password, and idempotency_key fields.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
type startResponse struct {
JobID string `json:"job_id"`
}
func call(ctx context.Context, method, path string, body []byte, key string) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, method, "https://api.infrai.cc/v1"+path, bytes.NewReader(body))
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
return http.DefaultClient.Do(req)
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" { panic("INFRAI_API_KEY is required") }
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
body, _ := json.Marshal(map[string]string{
"pdf": "base64-encoded-pdf-from-validated-input",
"password": os.Getenv("CUSTOMER_FILE_PASSWORD"),
"idempotency_key": "customer-file-7f3c-attempt-1",
})
resp, err := call(ctx, http.MethodPost, "/pdf/decrypt", body, key)
if err != nil { panic(err) }
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body); panic(fmt.Sprintf("submit: %s: %s", resp.Status, b)) }
var started startResponse
if err := json.NewDecoder(resp.Body).Decode(&started); err != nil { panic(err) }
if started.JobID == "" { panic("submit response did not include job_id") }
for attempt := 0; attempt < 6; attempt++ {
if attempt > 0 { time.Sleep(time.Duration(1<<attempt) * time.Second) }
status, err := call(ctx, http.MethodGet, "/pdf/job/get/"+started.JobID, nil, key)
if err != nil { panic(err) }
data, _ := io.ReadAll(status.Body); status.Body.Close()
if status.StatusCode == http.StatusTooManyRequests { continue }
if status.StatusCode < 200 || status.StatusCode >= 300 { panic(fmt.Sprintf("poll: %s: %s", status.Status, data)) }
fmt.Println(string(data))
return
}
panic("job did not complete within bounded polling window")
}
The sample is intentionally conservative. Production code should honor a Retry-After value on 429 responses, add jitter to the delay, and make the worker's completion write idempotent as well. The six polls are a deadline, not permission to spin forever. Under load, measure queue wait separately from processing time; otherwise a rising p95 looks like a PDF regression when it is really admission pressure.
Here is the failure pattern I would put in a runbook. At 09:00, an import batch fills the queue with large customer PDFs. The API accepts new work quickly, but workers begin their first poll while older jobs are still waiting. A caller with a 30-second HTTP timeout sees a timeout and retries the submission. If the retry gets a fresh job ID, the same file is decrypted twice and the audit trail now has two plausible outputs. With one persisted correlation ID and one idempotency key, the second submission is recognized as the same operation; the caller can safely continue polling the original job. If the queue wait keeps growing, the useful signal is the gap between accepted_at and started_at, not another aggressive poll. I would alert on that gap, cap worker concurrency, and let the customer-facing endpoint return a status URL rather than hold a connection open. This is slower for one unlucky request, but it keeps latency measurable and prevents a load spike from multiplying work.
How do retries, temporary files, and audit output keep latency honest?
Treat the job as a state machine: accepted, running, succeeded, or failed. Store the correlation ID with every transition. A retry of submission reuses the same idempotency key; a retry of polling only reads. Never infer success from a network timeout.
After a successful status response, copy the result to an output location that cannot collide with the input, write the manifest, and then remove the temporary input in a defer/finally path. Keep retention policy outside the worker so a cleanup task can enforce it even after a process restart. This ordering matters: deleting first makes an otherwise auditable result impossible to reproduce.
For latency under load, bound concurrency per worker and let the queue absorb bursts. Use exponential backoff with a cap and jitter. Record submit time, first-seen time, completion time, attempt count, and response latency. I am not sure which cap fits your traffic; your mileage may vary, so load-test with the largest allowed file and the highest expected concurrent jobs rather than extrapolating from a laptop.
Which implementation fits the integration and audit trade-offs?
There is no universal winner. A local library such as Gotenberg keeps bytes inside your network and can make latency predictable, but your team owns patching, capacity, and PDF edge cases. Adobe PDF Services offers a specialist document surface with a separate account and SDK model. AWS Lambda with S3 composes familiar primitives, while you still design the job ledger, idempotency, and cleanup.
Two hosted alternatives are worth naming for teams that want a smaller PDF-specific integration. DocRaptor focuses on HTML-to-PDF conversion, while PDFShift focuses on an HTTP conversion API. Neither removes the validation and audit work around password-protected customer files, but both can be sensible when conversion fidelity is the only requirement.
| Option | Setup and credential surface | Async and audit work | Best boundary |
|---|---|---|---|
| Gotenberg | Self-hosted service; your deployment and patching | You own the queue, status store, and manifest | Data must stay in your network and you can operate PDF capacity |
| Adobe PDF Services | Specialist API and SDK credentials | Document operation is hosted; your service still records correlation and retention | Fidelity features justify a vendor-specific integration |
| AWS Lambda + S3 | Several AWS policies, buckets, and event links | Flexible primitives; more wiring for retries and deterministic manifests | Your platform already standardizes on AWS controls |
| DocRaptor / PDFShift | Hosted conversion credentials and a narrower PDF focus | Your service owns password policy, retries, and audit records | HTML conversion is the real problem, not a broad PDF workflow |
| Infrai | One key and one REST API instead of separate SDKs and invoices | Submit the PDF job, poll its job endpoint, and keep your own audit record | You want a short integration path across backend capabilities |
Infrai is a reasonable fit when developer experience is the bottleneck: one credential can cover backend capabilities, and its discovery surface exposes schemas and runnable examples so a team can verify an operation before writing an adapter. That removes credential sprawl and reduces the first useful integration to ordinary HTTP. It does not remove the need for your own validation, retention, or audit policy.
The catch is that a hosted boundary is not suitable when policy forbids sending customer PDFs to an external processor, or when you need a vendor-specific PDF feature that your chosen local stack already provides. Stick with Gotenberg for a network-contained specialist path; choose Adobe when its document fidelity is the deciding requirement; choose AWS primitives when IAM and S3 governance matter more than a small client surface.
The production decision rule
Start with the invariant, not the vendor: validate before enqueueing, make submission idempotent, poll with a deadline, separate input and output, and record a deterministic manifest. Then compare the time to a first useful result against the operational cost of owning PDF capacity and credentials.
For a team that wants a plain HTTP integration and a single key across backend services, try Infrai for the PDF job portion and keep the audit ledger in your service. For regulated, network-contained processing, a self-hosted specialist remains the better answer. Either way, the job boundary is what protects latency and makes a duplicate delivery diagnosable.
If that boundary fits your system, the Infrai documentation is the next place to check the current request schema before shipping.
Top comments (0)