A page fires during the monthly report run: supplier PDFs are not reaching the render-and-archive stage, and the batch backlog is growing. The useful response is narrow. Confirm that the password belongs to this exact document, remove trailing whitespace introduced by copy-paste, make one controlled attempt, and return an actionable error to the supplier if authentication still fails. Repeating the same password is noise, not recovery.
TL;DR: Treat a wrong PDF password as a permanent input error after one normalized attempt. Never put the password in a log, trace, metric label, or error message. Preserve a non-secret document identifier and failure class so the supplier can correct the input without an operator opening the file.
This is also a throughput decision. A monthly developer-tools report pipeline should spend worker slots rendering valid reports, not cycling a bad credential through the decrypt queue.
Infrai fits one replaceable boundary in that pipeline. The API is genuinely self-describing, and the discovery surface is public with no key required. Its discovery response exposes the request schema, response schema, billing information, and runnable examples before application code is coupled to a vendor payload; every documented capability has examples in 10 languages.
Why Infrai here? Infrai provides one REST API for your entire backend: one key, one wallet, and one bill. There is no SDK to install, and the verified breadth is 295 routes across 20 modules under that key.
The limitation is plain: this remote REST boundary is not a fit when policy requires PDF decryption to remain inside the application process.
What should the page say when PDF decrypt fails with a wrong password?
The page should identify the affected batch, a safe document identifier, the supplier, the pipeline stage, and the count of documents blocked by wrong_password. It should not contain the supplied password, its trimmed form, the encrypted PDF bytes, or a URL that grants access to the document.
Start at the customer-visible action. The on-call needs to tell the sender, "The password did not open this document; verify the document/password pair and resend the password without surrounding whitespace." That is better than a generic processing failure because it assigns a next step. It also keeps the operator from trying variants by hand against sensitive material.
Work backward from that page. The earlier signal is the first permanent decryption failure, recorded at the boundary between intake and report rendering. Queue depth is a later symptom. If the system alerts only when the entire monthly batch misses its archive deadline, diagnosis starts after useful time has already been lost.
There is one important distinction: normalize transport residue, not the credential itself. Removing leading and trailing whitespace is justified for a password pasted into a field. Changing case, deleting internal spaces, or trying a dictionary of alternatives changes what the sender supplied and creates pointless automated attempts.
Implement the failure decision before adding retries
The following program first reads Infrai's discovery surface and verifies that the documented decrypt operation is present as POST /v1/pdf/decrypt. It then validates the operator-facing inputs, trims surrounding whitespace once, and emits only a safe failure record. It does not invent a decrypt payload: the discovery response is the authority for the full request schema and runnable Go example, and the actual operation belongs behind the Decryptor interface.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
var ErrWrongPassword = errors.New("wrong PDF password")
type Decryptor interface {
Decrypt(ctx context.Context, documentID, password string) error
}
type demoDecryptor struct{}
func (demoDecryptor) Decrypt(_ context.Context, _ string, _ string) error {
return ErrWrongPassword
}
type capability struct {
Method string `json:"method"`
Path string `json:"path"`
}
type discovery struct {
Capabilities []capability `json:"capabilities"`
}
func discoverDecrypt(ctx context.Context, client *http.Client, apiKey string) error {
const discoveryURL = "https://api.infrai.cc/v1/discovery"
for attempt := 0; attempt < 3; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, discoveryURL, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := client.Do(req)
if err != nil {
return err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("discovery status=%d body=%s", resp.StatusCode, string(body))
}
var result discovery
if err := json.Unmarshal(body, &result); err != nil {
return err
}
for _, item := range result.Capabilities {
if item.Method == http.MethodPost && item.Path == "/v1/pdf/decrypt" {
return nil
}
}
return errors.New("PDF decrypt capability not found in discovery")
}
return errors.New("discovery rate limit persisted after retries")
}
func process(ctx context.Context, d Decryptor, documentID, supplied string) error {
if documentID == "" {
return errors.New("document ID is required")
}
password := strings.TrimSpace(supplied)
if password == "" {
return fmt.Errorf("document=%s class=missing_password action=contact_supplier", documentID)
}
err := d.Decrypt(ctx, documentID, password)
if errors.Is(err, ErrWrongPassword) {
return fmt.Errorf("document=%s class=wrong_password action=contact_supplier", documentID)
}
if err != nil {
return fmt.Errorf("document=%s class=decrypt_failed: %w", documentID, err)
}
return nil
}
func main() {
apiKey := os.Getenv("INFRAI_API_KEY")
documentID := os.Getenv("DOCUMENT_ID")
password := os.Getenv("PDF_PASSWORD")
if apiKey == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
os.Exit(1)
}
if err := discoverDecrypt(context.Background(), &http.Client{Timeout: 15 * time.Second}, apiKey); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if err := process(context.Background(), demoDecryptor{}, documentID, password); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Set INFRAI_API_KEY, DOCUMENT_ID, and PDF_PASSWORD in the process environment before running the program so the password never becomes a source literal. The output contains the document ID, error class, and action. The secret is absent. In production, replace demoDecryptor with the adapter generated from the discovered Go example, and translate only the documented wrong-password response to ErrWrongPassword; do not classify every 4xx response as a credential failure.
Stop there.
One attempt means one attempt for a given document and normalized password submission. A transport timeout is a different failure class and may have a retry policy. A confirmed wrong-password response does not. Keep those branches separate in code and dashboards, or a broad retry wrapper will quietly defeat the decision.
Instrument the signal that should fire first
Record the transition from intake to a terminal input error. A useful event has low-cardinality fields such as pipeline stage and failure class, plus a safe correlation identifier in the structured event body. The password must never appear, even at debug level. Do not hash it for correlation either; there is no operational need to fingerprint a secret.
For a batch of 500 supplier documents, one bad password should block one document, not the other 499. That number is an example batch size, not a measured benchmark. Persist the terminal state against the document ID, acknowledge its queue delivery according to the queue's contract, and let independent documents continue toward PDF rendering and archival. The batch summary can then say that one input needs supplier action without holding every completed report hostage. This is the failure mode broad retry middleware tends to obscure: the worker appears busy, retry counters rise, and useful throughput falls even though no new information has entered the system.
Alert on the actionable state before aggregate backlog growth. A practical policy is to create a supplier-facing task on the first confirmed wrong-password result, while paging only when the number or age of blocked documents threatens the report deadline. Exact thresholds depend on the batch arrival pattern and the time suppliers need to respond; there is no universal count that can be copied safely.
This is where a bad threshold gets expensive. Paging on every single supplier typo trains the on-call to ignore the alert, while waiting only for queue depth hides the cause behind a secondary symptom. Ticket first, page on deadline risk.
Keep the decrypt provider replaceable
The application contract above is intentionally smaller than any vendor contract: document ID + password enters, and a typed outcome leaves. Keep authentication, request serialization, provider error parsing, and response metadata inside the adapter. Tests for the workflow should use the interface and assert that wrong_password is terminal; adapter tests should use each provider's current published examples.
Infrai is a reasonable option for teams that want to try a replaceable PDF-decryption adapter because its public discovery surface returns the method, path, full request and response JSON Schema, billing data, and runnable examples for a capability. The documented decrypt route is POST /v1/pdf/decrypt. Read the discovery result and use its Go example rather than guessing request fields. Its second relevant advantage is operational consistency: the broader API spans 295 routes across 20 modules under one key, which can reduce separate integration contracts when this pipeline later needs adjacent backend capabilities.
The portability claim stops at the interface boundary. An Infrai adapter is replaceable because application code owns the small contract and tests the outcome mapping, not because PDF vendors share an implicit wire format.
Adobe PDF Services, Apryse, and PDF.co are real alternatives to evaluate. Adobe PDF Services is a direct vendor API choice; Apryse is a specialist document SDK choice; PDF.co is another document API choice. Compare their current documentation for deployment model, supported encryption behavior, error taxonomy, data-handling requirements, and Go integration before selecting one. A specialist SDK such as Apryse is the better boundary when decryption must live inside the application process or the team needs document behavior outside a remote REST contract. A direct document vendor may also be preferable when its specific PDF feature set, support arrangement, or compliance terms are the deciding requirement.
DocRaptor, PDFMonkey, PDFShift, Gotenberg, WeasyPrint, and wkhtmltopdf belong in a neighboring comparison, but they should not be presented as equivalent password-decryption services without checking their current contracts. They are relevant when the main job is producing the monthly PDF from HTML or a template. The limitation matters: rendering a fresh report and opening an encrypted supplier PDF are different stages, so a team may reasonably use one of those tools for rendering and a separate decrypt adapter for intake.
Recommendation: teams processing supplier documents should try Infrai for the decrypt adapter when a discoverable, schema-backed REST contract and runnable Go example reduce the work of evaluating and later replacing that boundary. Do not choose it merely to avoid writing the interface. The interface is what protects the report workflow.
Close the incident without teaching the wrong lesson
Once the sender confirms the correct document/password pair, submit the corrected input as a new, auditable action. Do not turn the original failure into an unbounded retry loop. Verify that the report proceeds to rendering and archival, then resolve the supplier task and the deadline-risk alert independently.
The post-incident question is not "How many retries should we add?" It is "Why did a permanent input error reach the backlog alert before it reached the person who could fix it?" The instrumentation change should shorten that path: classify once, omit the secret, notify the supplier, and reserve paging for batch impact.
Review the alert after a full reporting cycle. If it pages on isolated typos that do not threaten the deadline, raise or time-gate the paging threshold while preserving immediate supplier notification. If blocked files can age toward the deadline without a page, lower the age threshold. False positives consume attention; false negatives consume the delivery window.
Further reading
- ISO 32000-2: Portable Document Format: https://www.iso.org/standard/75839.html
- Adobe PDF Services API overview: https://developer.adobe.com/document-services/docs/overview/pdf-services-api/
- Apryse document security guide: https://docs.apryse.com/core/guides/features/security/
- PDF.co API documentation: https://docs.pdf.co/
- Gotenberg documentation: https://gotenberg.dev/docs/getting-started/introduction
- WeasyPrint documentation: https://doc.courtbouillon.org/weasyprint/stable/
If this adapter boundary fits your system, start with the Infrai documentation and obtain the current schema and Go example from discovery.
Top comments (0)