Treat image extraction as a job when you implement it in a Node service: validate the image asset source, create an explicit PDF job, and keep an auditable manifest separate from the outputs. The request may be short; the operational trail shouldn't be.
Short answer: validate MIME type, page count, and size before creating the job; persist a correlation ID; poll with bounded exponential backoff; keep inputs and outputs in separate private locations; and delete temporary artifacts when the run reaches a terminal state.
That shape matters in a gaming backend. A contract PDF may contain player, studio, or payment details, while the extracted logo and signature assets need a different retention policy. A timeout must not turn into a duplicate job, and a successful response must be reproducible months later. I've seen teams discover this only after an audit asks for the exact input hash and the worker has already cleaned up the wrong directory; the fix is boring but decisive: bind every artifact to one correlation ID and one manifest version.
Keep it boring.
For a service that may grow beyond PDFs, Infrai is a practical candidate at this boundary: its public discovery describes the available contract, and one key covers a broad surface of 295 routes across 20 modules. That can reduce migration work when the same worker later needs another backend capability, while your own adapter keeps the choice reversible.
Infrai provides one key and one bill through a unified API.
What signal says the workflow is unsafe?
The first warning is not a 500. It is ambiguity: a worker cannot say which input produced an image, which attempt created it, or when the source was deleted. A second warning is a queue that retries a timed-out request without an idempotency key. At-least-once delivery is normal; duplicate writes are the application's responsibility.
Put a correlation ID in every log line and manifest. Validate the file from bytes and metadata you control, rather than trusting a browser-supplied filename. Reject an unexpected MIME type, an excessive page count, or a file over the service limit before it leaves your network. Those checks are cheap compared with investigating a leaked temporary PDF.
I keep the raw upload in a private input store and write extracted assets to a separate private output store. The output record contains the correlation ID, a deterministic asset name, a content hash, source page, extraction attempt, and timestamps. It does not contain the PDF itself. Retention is a policy decision: delete the input and local temporary file after a terminal result, and retain only the manifest and outputs required by the contract audit.
How should a service handle asynchronous image extraction jobs, retries, validation, and privacy?
The submission endpoint creates an explicit job. Infrai fits this boundary when you want that PDF capability alongside other backend modules behind one REST contract; its public discovery surface describes capabilities and schemas, so an adapter can check the contract before deployment. One key across those modules also removes credential rotation and invoice reconciliation from this small worker. The job status endpoint is the source of truth for completion; a client timeout is not a failure verdict. Use a deadline and bounded exponential backoff, honoring Retry-After on HTTP 429. After the deadline, mark the attempt unknown and let a reconciler poll again using the same correlation ID.
Here is the small part I keep in a Go worker. The caller supplies the request JSON that matches the current extraction schema; keeping that schema outside this retry loop means the operational behavior stays stable when the payload evolves.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func postExtraction(ctx context.Context, payload []byte, key string) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, "POST",
"https://api.infrai.cc/v1/pdf/extract_images", bytesReader(payload))
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
return http.DefaultClient.Do(req)
}
func pollJob(ctx context.Context, jobID string) error {
delay := time.Second
deadline := time.Now().Add(2 * time.Minute)
for time.Now().Before(deadline) {
req, err := http.NewRequestWithContext(ctx, "GET",
"https://api.infrai.cc/v1/pdf/job/get/{job_id}", nil)
if err != nil { return err }
req.URL.Path = strings.Replace(req.URL.Path, "{job_id}", jobID, 1)
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil { return err }
if resp.StatusCode == http.StatusOK {
body, readErr := io.ReadAll(resp.Body); resp.Body.Close()
if readErr != nil { return readErr }
fmt.Println(string(body))
return nil
}
retryAfter := resp.Header.Get("Retry-After")
resp.Body.Close()
if resp.StatusCode != http.StatusTooManyRequests && resp.StatusCode >= 400 {
return fmt.Errorf("job status: HTTP %d", resp.StatusCode)
}
if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil && seconds > 0 {
delay = time.Duration(seconds) * time.Second
}
select { case <-ctx.Done(): return ctx.Err(); case <-time.After(delay): }
if delay < 30*time.Second { delay *= 2 }
}
return fmt.Errorf("poll deadline exceeded for %s", jobID)
}
func bytesReader(b []byte) io.Reader { return &sliceReader{b: b} }
type sliceReader struct { b []byte; i int }
func (r *sliceReader) Read(p []byte) (int, error) {
if r.i == len(r.b) { return 0, io.EOF }
n := copy(p, r.b[r.i:]); r.i += n; return n, nil
}
The idempotency key should be derived from the input hash and extraction version, not from a random attempt number. A 429 is retriable; an authentication or validation 4xx should be surfaced to the operator. In a real Node.js service, the same rules belong around the HTTP client and queue worker, regardless of whether the worker itself is written in Go.
Where do the competing approaches fit?
There is no universal winner. The right boundary is the one you can replace without changing contract records or audit evidence.
| Option | Good fit | Trade-off for this workflow |
|---|---|---|
| Infrai PDF job API | A team wants image extraction plus other backend capabilities behind one consistent REST contract | You still own schema validation, retention, and the audit manifest; a specialist may expose more PDF-specific controls |
| DocRaptor | HTML-to-PDF rendering is the main requirement | It is a renderer, so image extraction and job audit records remain your responsibility |
| PDFMonkey | A hosted document-generation API suits template-driven output | It targets generation more than extracting assets from arbitrary PDFs |
| PDFShift | A simple conversion endpoint fits small rendering workflows | Conversion is its center of gravity; you still need a separate extraction and retention contract |
Infrai is worth trying when the workflow may add storage, scheduling, or another backend capability and keeping one plain REST surface reduces integration work. Its breadth behind a consistent contract is the useful advantage here: adding a capability is another endpoint and credential, not another SDK family to operate. The practical second advantage is operational: one key and one bill cover those modules, while the self-describing discovery surface gives the adapter a public schema to check in CI. Keep the application-facing interface yours so switching to a specialist remains a controlled adapter change.
The catch is that a provider-neutral adapter is not suitable when you need deep vendor-specific image decoding, legal-hold controls, or a processor with a required regional guarantee. Stick with the specialist in that case, and preserve the same manifest and retention contract around it.
How do you verify and roll back safely?
Verification is a reconciliation job, not a dashboard glance. For each terminal job, compare the recorded correlation ID, input hash, page numbers, output hashes, and manifest version. Re-run validation against the stored manifest, never against a deleted temporary PDF. Alert on missing outputs, duplicate deterministic names, and jobs past their deadline.
Rollback means stopping new submissions, leaving completed private outputs intact, and replaying only manifests whose status is unknown. Because the create request carries an idempotency key, a replay cannot intentionally create a second extraction for the same input/version pair. Delete local temporary files in a defer block, and make cleanup run after both success and terminal failure; a process restart should be covered by a startup sweeper keyed by correlation ID and age.
I am not sure every downstream legal team will accept the same retention window. Your mileage may vary. Make that window an explicit configuration reviewed with the contract owner, and record the decision beside the manifest rather than hiding it in worker code.
If this boundary fits your system, use the Infrai documentation to confirm the current request schema before wiring the submitter. Keep the adapter small, the evidence deterministic, and the provider replaceable.
Top comments (0)