I like browser-only image processing.
For local files, the architecture can be beautifully simple:
user's disk
↓
browser
↓
decode
↓
convert
↓
download
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/*"
/>
the user explicitly gives your page access to selected files.
You can create object URLs:
const url =
URL.createObjectURL(file);
decode the asset, draw it, and process it.
With a remote URL:
https://merchant-cdn.example/product-123.jpg
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
Your first attempt might be:
const response =
await fetch(imageUrl);
const blob =
await response.blob();
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
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"
});
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
into:
full image bytes available
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
This is excellent when you control the source CDN.
For example, your own infrastructure can return:
Access-Control-Allow-Origin: https://yourapp.example
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
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()
);
});
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
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
"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
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
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
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;
}
That catches obvious bad data before the remote job starts.
Queueing matters at 500 URLs
Do not launch:
await Promise.all(
urls.map(processUrl)
);
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
)
);
}
Now you can tune:
network concurrency
CPU conversion concurrency
memory usage
provider rate limits
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
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
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
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!
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.
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
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)