One day I noticed that the plant images on House Plants Of The World weren't loading. Not all of them, just some, and there was no obvious pattern at first glance. All the plant data (latin names, common names, country info) was showing up correctly. Only the images were broken.
Opening the browser's Network tab showed many image requests were failing with:
net::ERR_BLOCKED_BY_ORB
The failing requests were all fetching from upload.wikimedia.org.
The Diagnosis
Digging into the failed requests revealed the real error underneath ERR_BLOCKED_BY_ORB: a 429 Too Many Requests response from Wikimedia's CDN servers.
The response headers confirmed it:
Status Code: 429 Too Many Requests
retry-after: 10
server: Varnish
x-cache-status: int-front
And the request headers showed why Wikimedia was flagging these:
referer: https://www.houseplantsoftheworld.com/
sec-fetch-site: cross-site
sec-fetch-mode: no-cors
I think Wikimedia's Varnish CDN was identifying these as hotlink requests from an external site and rate
limiting them. ERR_BLOCKED_BY_ORB (Opaque Response Blocking) is a Chrome security feature that blocks
cross-origin no-cors requests when the server returns an error status. So the 429 from Wikimedia triggered
Chrome to block the response entirely. Some images that still appeared to load were probably being served from the
browser's cache, leftovers from before the rate limiting kicked in. When I checked a couple of days later, those had failed too.
Or, possibly Wikimedia's url schema or access permissions had changed. When I manually entered the image url into a browser search, I got a blank white page with this error message: "Use thumbnail steps listed on https://w.wiki/GHai. Varnish XID". Regardless, it was time to adapt.
The Root Cause
The images were stored in MongoDB as direct CDN thumbnail URLs in this format:
https://upload.wikimedia.org/wikipedia/commons/thumb/{a}/{ab}/{filename}/{width}px-{filename}
This is the internal CDN path that Wikimedia uses to serve thumbnails. Back when I built this app in late 2024, this was a valid method of using Wikimedia Commons assets. Wikimedia themselves even says hotlinking is allowed in their documentation, though they note the risks of vandalism, renaming, or deletion. Hotlinking was a great option for me despite those risks, because it allowed me to get a quick prototype of the app going. However, sometime between December 2025 and March 2026, Wikimedia changed their policy, endpoint, or CDN configuration, and the URL format I was using started bypassing their proper public-facing endpoint, hitting their Varnish cache layer directly, and triggering rate limit errors.
The new, correct way to hotlink Wikimedia Commons images is via the Special:Redirect endpoint, as documented in the official Wikimedia technical reuse guide:
https://commons.wikimedia.org/w/index.php?title=Special:Redirect/file/{filename}&width={width}
For example, instead of:
https://upload.wikimedia.org/wikipedia/commons/thumb/8/84/Jade_Plant%2C_Crassula_ovata_IMG_3632.jpg/320px-Jade_Plant%2C_Crassula_ovata_IMG_3632.jpg
The correct hotlink URL is:
https://commons.wikimedia.org/w/index.php?title=Special:Redirect/file/Jade_Plant%2C_Crassula_ovata_IMG_3632.jpg&width=320
This routes through the sanctioned Wikimedia endpoint, which handles redirects to the appropriate CDN resource and is designed for external use. I tried it out in the MongoDB Data Explorer and it worked! The image was instantly restored when I refreshed the web app. So that was promising.
The Fix
The fix was a targeted MongoDB migration. No application code changes required. Most of my images were from open source
Wikimedia Commons contributions, with a few from iNaturalist that didn't need to change. So I would need to sort through the
database and update all the image URLs that matched the old Wikimedia CDN pattern, transforming them into the new Special:Redirect format.
Backup
Before touching anything, I exported the entire houseplants collection to a timestamped JSON file using a small Node.js script:
// scripts/backup.js
import { writeFileSync } from "fs";
import connectToDatabase from "../db/connection.js";
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const outputPath = `./backups/houseplants-${timestamp}.json`;
async function backup() {
try {
const db = await connectToDatabase();
const collection = db.collection("houseplants");
const docs = await collection.find({}).toArray();
writeFileSync(outputPath, JSON.stringify(docs, null, 2));
console.log(`✓ Backed up ${docs.length} documents to ${outputPath}`);
} catch (err) {
console.error("Backup failed:", err);
process.exit(1);
}
}
backup();
node scripts/backup.js
// Output: backups/houseplants-2026-03-14T03-39-27-274Z.json
Just dump it all to a file so there's a safety net.
Migration Script
The migration script does four things: queries only documents where imageUrl matches the old Wikimedia
CDN pattern, parses each URL with a regex to extract the width and filename, constructs the new Special:Redirect URL,
and collects all changes into a single bulkWrite operation.
// scripts/migrate-image-urls.js
import connectToDatabase from "../db/connection.js";
const DRY_RUN = true; // Set to false to apply changes
function transformUrl(oldUrl) {
const lastSegment = oldUrl.split("/").pop();
const match = lastSegment.match(/^(\d+)px-(.+)$/);
if (!match) return null;
const [, width, filename] = match;
return `https://commons.wikimedia.org/w/index.php?title=Special:Redirect/file/${filename}&width=${width}`;
}
async function migrate() {
const db = await connectToDatabase();
const collection = db.collection("houseplants");
const docs = await collection
.find({ imageUrl: { $regex: /^https:\/\/upload\.wikimedia\.org\// } })
.project({ _id: 1, name: 1, imageUrl: 1 })
.toArray();
console.log(`Found ${docs.length} Wikimedia URLs to migrate.\n`);
const operations = [];
const warnings = [];
for (const doc of docs) {
const newUrl = transformUrl(doc.imageUrl);
if (!newUrl) {
warnings.push({ _id: doc._id, name: doc.name, imageUrl: doc.imageUrl });
continue;
}
console.log(`${doc.name}`);
console.log(` OLD: ${doc.imageUrl}`);
console.log(` NEW: ${newUrl}\n`);
operations.push({
updateOne: {
filter: { _id: doc._id },
update: { $set: { imageUrl: newUrl } },
},
});
}
if (warnings.length > 0) {
console.log("Could not parse the following URLs (skipped):");
for (const w of warnings) {
console.log(` ${w.name}: ${w.imageUrl}`);
}
console.log();
}
if (DRY_RUN) {
console.log(
`DRY RUN no changes written. ${operations.length} documents would be updated.`
);
console.log(`Set DRY_RUN = false to apply.`);
return;
}
if (operations.length === 0) {
console.log("Nothing to update.");
return;
}
const result = await collection.bulkWrite(operations, { ordered: false });
console.log(`Migration complete: ${result.modifiedCount} documents updated.`);
// Spot-check: log 3 updated docs
const sample = await collection
.find({
_id: { $in: operations.slice(0, 3).map((op) => op.updateOne.filter._id) },
})
.project({ name: 1, imageUrl: 1 })
.toArray();
console.log("\nSpot-check (3 updated docs):");
for (const doc of sample) {
console.log(` ${doc.name}: ${doc.imageUrl}`);
}
}
migrate().catch((err) => {
console.error("Migration failed:", err);
process.exit(1);
});
The transformUrl function is the heart of it. It grabs the last path segment of the old URL and runs a regex against it: /^(\d+)px-(.+)$/. Capture group 1 is the width (e.g. "320"), capture group 2 is the filename (e.g. "Aglaonema_commutatum_kz1.jpg"). From those two pieces it constructs the new Special:Redirect URL.
Dry Run
With DRY_RUN = true, the script logged every old → new URL transformation for all affected documents without writing anything. I reviewed the output to confirm correct parsing across edge cases: filenames with numbers, dashes, parentheses, percent-encoded characters, and mixed-case extensions (.jpg vs .JPG).
Apply and Verify
With DRY_RUN = false:
node scripts/migrate-image-urls.js
I checked the live app immediately after. All plant images loaded correctly. Houseplantsoftheworld.com is back in action.
Takeaways
Don't hotlink internal CDN paths. The upload.wikimedia.org/wikipedia/commons/thumb/ URL structure is not a public API. Use the documented Special:Redirect endpoint instead.
Always dry run before bulk writes. Verifying transformations before applying them prevented any risk of corrupting data, and the backup JSON provided an additional safety net.
ERR_BLOCKED_BY_ORB is a symptom, not a cause. The real error was the 429 from Wikimedia. ORB is Chrome enforcing security policy on top of that. Tracking down the underlying HTTP status code was the key to diagnosing the real problem.