DEV Community

Cover image for Bulk-Converting Image URLs from CSV: Why CORS Breaks the Browser-Only Approach
Muhaymin Bin Mehmood
Muhaymin Bin Mehmood

Posted on

Bulk-Converting Image URLs from CSV: Why CORS Breaks the Browser-Only Approach

I like browser-only image processing.

For local files, the architecture can be beautifully simple:

user's disk
    ↓
browser
    ↓
decode
    ↓
convert
    ↓
download
Enter fullscreen mode Exit fullscreen mode

No upload.

No temporary server storage.

No queue worker.

Then someone asks:

Can I upload a CSV containing 500 product image URLs and convert all of those too?

At first, it sounds like the same feature with a different input source.

It isn't.

The moment the images live on arbitrary remote domains, your browser runs into the web's cross-origin security model.


Local files and remote URLs are different trust boundaries

With a local file input:

<input
  type="file"
  accept="image/*"
/>
Enter fullscreen mode Exit fullscreen mode

the user explicitly gives your page access to selected files.

You can create object URLs:

const url =
  URL.createObjectURL(file);
Enter fullscreen mode Exit fullscreen mode

decode the asset, draw it, and process it.

With a remote URL:

https://merchant-cdn.example/product-123.jpg
Enter fullscreen mode Exit fullscreen mode

your page does not automatically have permission to fetch and read that resource however it wants.

That is where CORS enters the architecture.


What happens with fetch()

Suppose a CSV contains:

sku,image_url
A-100,https://cdn-a.example/a.jpg
A-101,https://cdn-b.example/b.jpg
Enter fullscreen mode Exit fullscreen mode

Your first attempt might be:

const response =
  await fetch(imageUrl);

const blob =
  await response.blob();
Enter fullscreen mode Exit fullscreen mode

This works only when the target server's cross-origin policy allows the request.

If the server does not send suitable CORS headers, the browser blocks JavaScript from reading the response.

That is intentional.


Why browsers enforce this

Without same-origin restrictions, any random site you visit could potentially try to read resources from other services where your browser has credentials or access.

The same-origin policy is one of the web's fundamental security boundaries.

CORS is a controlled mechanism that lets a server say:

I explicitly allow this other origin to access my resource.

If the image server does not grant that permission, your frontend cannot simply override it.


The Canvas version of the problem

There is another variation.

Sometimes an external image can visually appear in a page, but drawing it to Canvas and attempting to read/export the Canvas causes a security error.

Conceptually:

remote image
    ↓
Canvas draws it
    ↓
Canvas becomes tainted
    ↓
toBlob / toDataURL blocked
Enter fullscreen mode Exit fullscreen mode

MDN explains this in:

Use cross-origin images in a canvas

This matters for image converters because export normally requires access to the resulting pixel data.


Why mode: "no-cors" does not solve it

A common Stack Overflow-style suggestion is:

fetch(url, {
  mode: "no-cors"
});
Enter fullscreen mode Exit fullscreen mode

That does not give you a readable image response.

It generally produces an opaque response that JavaScript cannot inspect like a normal cross-origin resource.

So this does not magically turn:

CORS denied
Enter fullscreen mode Exit fullscreen mode

into:

full image bytes available
Enter fullscreen mode Exit fullscreen mode

If your application needs the bytes, the remote server must allow the request or you need another architecture.


Architecture 1: fully browser-side when sources support CORS

The ideal case:

CSV
 ↓
browser parses rows
 ↓
fetch image URL
 ↓
remote CDN allows CORS
 ↓
Blob
 ↓
decode / convert
 ↓
download
Enter fullscreen mode Exit fullscreen mode

This is excellent when you control the source CDN.

For example, your own infrastructure can return:

Access-Control-Allow-Origin: https://yourapp.example
Enter fullscreen mode Exit fullscreen mode

or another appropriate policy.


Architecture 2: server-side fetch layer

For arbitrary public product-image URLs, you cannot assume every server has a browser-friendly CORS policy.

A common architecture becomes:

CSV
 ↓
browser parses / previews
 ↓
send URL job
 ↓
your backend fetches remote image
 ↓
validate response
 ↓
convert / normalize
 ↓
return output
Enter fullscreen mode Exit fullscreen mode

The server is not restricted by browser CORS in the same way because CORS is primarily a browser-enforced policy.

But now you have new security problems.


Server-side URL fetching creates SSRF risk

Do not build:

app.post("/fetch-image", async (req, res) => {
  const response =
    await fetch(req.body.url);

  res.send(
    await response.arrayBuffer()
  );
});
Enter fullscreen mode Exit fullscreen mode

and call it finished.

If users can make your server fetch arbitrary URLs, you may accidentally create a Server-Side Request Forgery surface.

An attacker could try:

http://localhost:...
http://127.0.0.1:...
http://169.254.169.254/...
private network hosts
redirect chains
unexpected protocols
Enter fullscreen mode Exit fullscreen mode

A production fetcher needs controls.


A safer remote-fetch checklist

At minimum, consider:

allow http/https only
resolve DNS carefully
block private/internal IP ranges
limit redirects
re-validate every redirect target
limit response size
limit time
validate content type
validate actual file signature
rate limit users
avoid forwarding user cookies
avoid forwarding internal credentials
Enter fullscreen mode Exit fullscreen mode

"Proxy the URL through the backend" sounds easy.

A secure fetch service is not trivial.


Validate actual image content

Do not trust only:

Content-Type: image/jpeg
Enter fullscreen mode Exit fullscreen mode

The server may be wrong or malicious.

You should enforce:

  • maximum bytes
  • expected media types
  • decodable image content
  • maximum dimensions
  • reasonable processing limits

A file that claims to be an image can still be dangerous from a resource-exhaustion perspective.


Parse the spreadsheet in the browser first

Even when remote fetching requires a backend, you can still keep useful work client-side.

For example:

Excel / CSV
     ↓
browser parses file
     ↓
show columns
     ↓
detect image URL column
     ↓
validate obvious row errors
     ↓
preview first rows
     ↓
user confirms job
     ↓
server processes URLs
Enter fullscreen mode Exit fullscreen mode

This is better UX than immediately uploading the spreadsheet and making the user guess what will happen.

It also reduces unnecessary server jobs.


Example CSV workflow

Suppose we support:

sku,image_url,format,quality
SKU-001,https://cdn.example/1.jpg,webp,82
SKU-002,https://cdn.example/2.png,webp,82
SKU-003,https://other.example/3.jpg,jpg,90
Enter fullscreen mode Exit fullscreen mode

The browser can validate:

function validateRow(row) {
  const errors = [];

  try {
    const url =
      new URL(row.image_url);

    if (
      !["http:", "https:"]
        .includes(url.protocol)
    ) {
      errors.push(
        "Unsupported URL protocol"
      );
    }
  } catch {
    errors.push(
      "Invalid image URL"
    );
  }

  if (
    !["webp", "jpg", "png"]
      .includes(row.format)
  ) {
    errors.push(
      "Unsupported format"
    );
  }

  return errors;
}
Enter fullscreen mode Exit fullscreen mode

That catches obvious bad data before the remote job starts.


Queueing matters at 500 URLs

Do not launch:

await Promise.all(
  urls.map(processUrl)
);
Enter fullscreen mode Exit fullscreen mode

against 500 large images unless your system is explicitly designed for that concurrency.

A queue gives you control.

async function runPool(
  items,
  worker,
  concurrency = 5
) {
  let index = 0;

  async function run() {
    while (true) {
      const current =
        index++;

      if (
        current >=
        items.length
      ) {
        return;
      }

      await worker(
        items[current]
      );
    }
  }

  await Promise.all(
    Array.from(
      { length: concurrency },
      run
    )
  );
}
Enter fullscreen mode Exit fullscreen mode

Now you can tune:

network concurrency
CPU conversion concurrency
memory usage
provider rate limits
Enter fullscreen mode Exit fullscreen mode

separately if needed.


Failure reporting is part of the feature

In a batch of 500 URLs, some will fail.

Reasons include:

  • 404
  • timeout
  • unsupported format
  • CORS
  • server rejection
  • image too large
  • corrupt file
  • DNS failure

Do not make the entire job fail because row 317 is bad.

Return a result report:

sku,status,error
SKU-001,success,
SKU-002,success,
SKU-003,failed,HTTP 404
Enter fullscreen mode Exit fullscreen mode

The user needs to know what to fix.


Why the browser-only rule should not become ideology

"Everything should happen locally" is a useful design preference.

It should not become a fake claim when the problem no longer fits.

For local files, browser-only conversion is a great architecture.

For arbitrary remote URLs, the browser's security boundary changes the problem.

The correct design can be hybrid:

Local files:
browser

Spreadsheet parsing:
browser

Remote URL fetch:
server when required

Preview:
browser

Final download:
browser
Enter fullscreen mode Exit fullscreen mode

That is not a failure of client-side architecture.

It is a consequence of the web security model.


This is exactly why my Excel/CSV workflow differs from normal BatchSet conversion

BatchSet's ordinary local-file conversion can process files in the browser.

But the Excel/CSV Image Converter has to deal with remote URLs that may live on arbitrary CDNs.

So the workflow is intentionally different:

spreadsheet
→ preview
→ account-backed remote batch
→ fetch images
→ convert
→ package output
Enter fullscreen mode Exit fullscreen mode

The live tool explains this distinction rather than pretending remote URL ingestion is identical to local processing.

If you already have a product catalog in Excel or CSV and want to test the workflow:

Open BatchSet's Excel/CSV Image Converter


Product insight: architecture can become a marketing advantage

Developers notice when a product explains its boundaries.

Instead of saying:

100% browser-only!
Enter fullscreen mode Exit fullscreen mode

for every feature, a more credible message is:

Local files stay local.

Remote URL jobs use a server fetch layer
when the browser cannot legally read
the remote source.
Enter fullscreen mode Exit fullscreen mode

That is both more accurate and more useful.

Users do not need slogans.

They need to know where their data goes.


Final takeaway

Bulk image conversion from a spreadsheet is not just:

loop over URLs
→ convert
Enter fullscreen mode Exit fullscreen mode

The moment your source images live on other domains, your architecture must account for:

  • CORS
  • Canvas tainting
  • safe remote fetching
  • SSRF
  • timeouts
  • validation
  • concurrency
  • partial failures

For local files, the browser can be the entire processing engine.

For arbitrary remote URLs, a hybrid client/server pipeline is often the more honest and robust design.

Top comments (0)