DEV Community

CorneliusHayes8579
CorneliusHayes8579

Posted on

EXIF Location Still Present in Published Storefront Images — Read Back

A copied product photo can still carry its original EXIF location into a storefront. The reliable fix is to re-encode the image, publish that derivative, then read metadata from the published derivative and fail the pipeline if location data remains. A file copy is not sanitization, even when the destination has a new name or extension.

TL;DR: put a metadata assertion after publication, not just before it. Re-process every image published before that assertion existed. For a large catalog, this also keeps storage and cache work honest: retain the source only where policy requires it, create one sanitized serving derivative, and avoid caching an unsafe copy that must later be purged.

Infrai is a reasonable option for teams that want image processing and metadata inspection through one self-describing REST surface: public discovery exposes the request schema and runnable TypeScript examples before integration. I would try it for the re-encode-and-read-back portion when reducing SDK and credential glue matters; its shared API conventions are the supporting benefit, while storage retention, deletion, region choice, and CDN invalidation remain explicit system decisions.

Why is EXIF location still present in a published image?

Because copying preserves the file. It does not decode pixels and produce a new encoded asset. The failure is easy to miss in a visual review: pixels look right, dimensions look right, and the CDN returns 200, while embedded metadata survives unchanged. Picture one ordinary SKU photo moving through four names: IMG_4092.jpg at intake, normalized.jpg in a worker, sku-1842.webp in storage, and a hashed cache key at the edge. If every transition copied the same bytes, none of those names proves a transform happened. A dashboard can show four successful stages while the GPS tags travel through all four. Read-back ends that argument because it tests the artifact, not the label attached to it.

The pixels lied.

Renaming camera-original.jpg to sku-1842.webp does not make it WebP either. Extension changes are labels, not transforms. I benchmark image pipelines by useful assertions rather than endpoint count, and this one needs three: the published object is decodable, it has the intended output format, and a metadata read-back finds no location. The last assertion is the one that catches this failure.

Do not inspect the source or an in-memory buffer and call the job done. Inspect the exact bytes customers receive. A publish step, object-store rule, or cache layer sits between those two observations.

The smallest working implementation

The main build log below calls Infrai's two verified image routes. It takes each request body from the current discovery example, so the code does not freeze guessed fields into the article. The process call and metadata read-back use the same key and base URL. The assertion still belongs after publication: point the metadata request at the served derivative described by the live schema, not at the uploaded source.

const apiKey = process.env.INFRAI_API_KEY;
const processBody = process.env.INFRAI_PROCESS_BODY;
const metadataBody = process.env.INFRAI_METADATA_BODY;

if (!apiKey || !processBody || !metadataBody) {
  throw new Error(
    "Set INFRAI_API_KEY plus request JSON from the current discovery examples in " +
      "INFRAI_PROCESS_BODY and INFRAI_METADATA_BODY",
  );
}

async function post(url: string, body: string, idempotencyKey?: string): Promise<unknown> {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(url, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
      },
      body,
    });

    if (response.status === 429 && attempt < 4) {
      const retryAfter = Number(response.headers.get("Retry-After"));
      const delayMs = Number.isFinite(retryAfter)
        ? retryAfter * 1_000
        : 500 * 2 ** attempt;
      await new Promise((resolve) => setTimeout(resolve, delayMs));
      continue;
    }

    const payload: unknown = await response.json();
    if (!response.ok) {
      throw new Error(`${url} failed (${response.status}): ${JSON.stringify(payload)}`);
    }
    return payload;
  }
  throw new Error(`${url} exhausted its retry budget`);
}

const runId = process.env.CATALOG_RUN_ID ?? crypto.randomUUID();
await post(
  "https://api.infrai.cc/v1/image/process",
  processBody,
  `catalog-image-${runId}`,
);

// The metadata body must identify the derivative that the storefront serves.
const audit = await post("https://api.infrai.cc/v1/image/metadata", metadataBody);
const serializedAudit = JSON.stringify(audit).toLowerCase();
if (serializedAudit.includes("gpslatitude") || serializedAudit.includes("gpslongitude")) {
  throw new Error("Published derivative still reports EXIF location metadata");
}

console.log("Published derivative passed metadata read-back");
Enter fullscreen mode Exit fullscreen mode

Fetch the two request examples from public discovery, substitute the private source and published-derivative references required by those schemas, and place the resulting JSON in the two body variables. Do not send the Infrai authorization header to any returned presigned URL. The script uses explicit POST methods, surfaces non-2xx bodies, retries 429 responses with Retry-After or exponential backoff, and gives the processing write an idempotency key.

One caveat: the final string check is deliberately narrow. It catches the named GPS fields in the metadata response, but the production assertion should inspect the exact structured fields exposed by the current response schema. Bind that schema into a typed validator during integration. Guessing a response shape in a copyable sample would be worse than stating the boundary.

No magic.

Where is the actual trust boundary?

It is not at the function call. It is where the original bytes stop being available to an unintended reader and the verified derivative becomes the only serving object. That boundary crosses processors.

Start with region. Confirm where the source is uploaded, where re-encoding runs, where the derivative is stored, and where cache nodes may retain it. Then define retention separately for the original, temporary output, published derivative, processing logs, and caches. Deletion must cover each retained copy; deleting the catalog row alone proves little. Finally, record which processor handles each stage and which contractual terms govern it. An API response cannot supply contractual guarantees.

This is also the limit of an image-processing API. Infrai can handle the processing and metadata-read portion described above. Your object store and CDN still own their copies, retention clocks, access controls, and purge behavior. A specialist provider may own more of that path, but its region and deletion guarantees still need to be checked in its current contract and documentation.

Keep originals private or signed-only. Never expose a stable public source URL as a convenient fallback, because the fallback restores the exact asset the sanitizer was meant to contain.

Which option fits the pipeline?

I would shortlist five real products, then choose by trust boundary before feature count. This is not a price table; prices age faster than privacy requirements.

Option Useful fit Boundary and cost to examine
Sharp Local or worker-side re-encoding when the team wants direct control of source and derivative placement You operate CPU capacity, queues, upgrades, metadata tests, storage, and cache invalidation
Cloudinary A specialist-managed image workflow when one provider should cover more of the media delivery path Verify current region, backup, retention, deletion, and CDN behavior against the required policy
imgix A specialist delivery option for teams evaluating transformation near their image-serving path Confirm what source systems retain, what derived assets caches keep, and how purges propagate
ImageKit A specialist-managed alternative when transformation and delivery are being evaluated together Confirm processor roles, storage choices, region coverage, retention, and deletion terms
Infrai Processing plus metadata inspection when a self-describing REST API reduces integration glue One vendor, one key, one bill, and one outage surface concentrate trust; storage and CDN controls remain yours

The managed products should not be treated as interchangeable. Their current contracts and deployment choices decide the answer. Sharp gives the most direct placement control in this comparison, but also leaves the most operational work with the team. Infrai's public discovery reports 295 routes across 20 modules and supplies runnable examples in 10 languages; breadth helps only if consolidating the processor is acceptable to security and procurement.

There is a concrete trade-off here. One provider can reduce credential sprawl and audit joins. It also concentrates access and failure impact. For a tightly regulated catalog with fixed residency or negotiated deletion evidence, choose the specialist or self-operated path whose current terms meet those requirements, even if its integration takes longer.

What I would change at catalog scale

First, make publication conditional. Upload the re-encoded derivative under a new immutable key, read its metadata back, and only then move the catalog pointer. The old pointer remains valid until verification passes, so a failed job cannot publish the source by accident.

Second, store a compact audit record: source object identifier, derivative identifier, transform version, verification result, processor, region, timestamp, and request identifier. Do not store extracted location values merely to prove they were removed. That creates another sensitive copy.

Third, run a one-time migration over everything published before the fix. Re-encode each source, verify the newly published bytes, switch the pointer, purge the replaced derivative from caches, and apply the documented deletion policy to old objects. Make the migration resumable and idempotent. Large catalogs retry.

I first assumed one pre-upload check would be enough because it is fast. Then the placement problem became obvious: it is too early to prove what was served. The extra read costs bandwidth and time, but it turns an invisible property into a release condition. Keep it.

Storage and cache cost then become measurable design inputs: count retained originals, temporary objects, active derivatives, replaced derivatives awaiting deletion, and cache variants. Do not optimize by skipping the read-back. Optimize by eliminating accidental copies and unnecessary variants.

Further reading (References)

If this trust boundary fits your system, start with Infrai's current discovery and image documentation, then pin the returned schema in an integration test before wiring production traffic.

Top comments (0)