Overview
In this tutorial, you'll learn why HTTP requests or API calls made directly from the browser fail with a "Failed to Fetch" error, and how to fix it using the proxy pattern.
Background
If you've ever tried to make an HTTP request or API calls to an external server directly from a frontend JavaScript app, you've likely seen this error:
It shows up in the browser console, but the request never goes through. It can feel confusing because the same call works fine in Postman or curl.
The root cause is not your code. It is a browser security mechanism called CORS, and understanding it is the first step to fixing it.
Same-Origin Policy
Browsers enforce a rule called the Same-Origin Policy. It restricts web pages from making requests to a different origin than the one that served the page.
Three things define an origin:
Protocol —
httporhttpsDomain —
localhostorexample.comPort —
3000,8080, etc.
If any of these three differ between your frontend and the server you're calling, the browser treats it as a cross-origin request.
For example:
| Frontend origin | API origin | Same origin? |
|---|---|---|
http://localhost:3000 |
http://localhost:3000 |
✅ Yes |
http://127.0.0.1:5500 |
https://www.youtube.com |
❌ No |
Cross-Origin Resource Sharing (CORS)
CORS is the mechanism that controls whether a cross-origin request is allowed.
When your browser makes a cross-origin request, it first sends a preflight request to ask the server: "Do you allow requests from my origin?" If the server doesn't respond with the right headers — Access-Control-Allow-Origin — The browser blocks the request entirely and throws the "Failed to Fetch" error.
💡 Note: This is a browser restriction, not an API restriction. The same request works from a server, Postman, or curl because those environments don't enforce the Same-Origin Policy.
Prerequisites
To follow along, make sure you have:
A basic understanding of HTML and JavaScript
Node.js v18 or higher installed
VS Code with the Live Server extension installed
Project Structure
We'll be working with a simple YouTube Video Info app — it takes a YouTube URL and attempts to fetch the page directly from the browser. It's a straightforward example that clearly demonstrates the "Failed to Fetch" CORS error.
Clone the broken version of the app:
git clone https://github.com/manueldezman/youtube-info.git
cd youtube-info
The project structure looks like this:
broken-version/
├── index.html # HTML structure
├── styles.css # Styling
└── app.js # Frontend logic — calls YouTube directly
To start the app:
Open the project folder in VS Code.
Click Go Live in the bottom right corner to start the Live Server(as shown below).
Your browser will open automatically at http://127.0.0.1:5500.
How a Direct Browser Call Triggers the CORS Error
Paste any YouTube URL into the input field and click Fetch Info. You'll see the button spin for a moment, and then an error appears.
To understand the error, open DevTools by right-clicking anywhere on the page and selecting Inspect, as shown in the media below.

Your console should look like this:
The Cause of the Error
Your frontend is running at http://127.0.0.1:5500 (Live Server). YouTube is at https://www.youtube.com. These are two completely different origins — different protocols, different domains, different ports. The browser sends a preflight request to YouTube asking if cross-origin requests are allowed. YouTube doesn't return the Access-Control-Allow-Origin header for direct page requests, so the browser blocks the request entirely before it even reaches YouTube.
Open app.js and look at the fetchInfo function:
async function fetchInfo() {
const url = document.getElementById('video-url').value.trim();
/* ⚠️ Direct browser-to-YouTube call — triggers CORS error */
const res = await fetch(url);
const html = await res.text();
...
}
The fetch(url) call goes directly to https://www.youtube.com from the browser, which triggers the CORS block. The request never reaches YouTube, and the app fails silently.
How to Fix the Error Using a Proxy Server
The fix is to stop calling YouTube directly from the browser and instead route the request through your own backend.
Your frontend is running at http://127.0.0.1:5500. YouTube is at https://www.youtube.com. These are two different origins — that's why CORS blocks the request.
The solution is to introduce a Node.js proxy server that sits between the two. Your frontend calls your own backend at http://localhost:3000/fetch. The Node.js server calls YouTube server-to-server — no browser, no CORS enforcement — fetches the page, extracts the video title and description from the HTML meta tags, and returns clean data back to the frontend.
Here's how to add the proxy to the project.
Step 1 — Create server.js
In the project root, create a new file called server.js:
broken-version/
├── index.html
├── styles.css
├── app.js
└── server.js ← add this
Step 2 — Add the proxy code
Paste the following into server.js:
const http = require('http');
const https = require('https');
const url = require('url');
const PORT = 3000;
function extractTitle(html) {
var match = html.match(/<title>([^<]*)<\/title>/i);
return match ? match[1].replace(' - YouTube', '').trim() : null;
}
function extractMeta(html, property) {
var match = html.match(new RegExp('<meta[^>]*(?:name|property)=["\']' + property + '["\'][^>]*content=["\']([^"\']*)["\']', 'i'));
if (match) return match[1];
match = html.match(new RegExp('<meta[^>]*content=["\']([^"\']*)["\'][^>]*(?:name|property)=["\']' + property + '["\']', 'i'));
return match ? match[1] : null;
}
const server = http.createServer(function(req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
var parsed = url.parse(req.url, true);
var videoUrl = parsed.query.url;
if (parsed.pathname !== '/fetch') {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not found' }));
return;
}
if (!videoUrl) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Missing url parameter' }));
return;
}
https.get(videoUrl, { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; proxy/1.0)' } }, function(youtubeRes) {
var body = '';
youtubeRes.on('data', function(chunk) { body += chunk; });
youtubeRes.on('end', function() {
var title = extractTitle(body) || extractMeta(body, 'og:title') || 'N/A';
var description = extractMeta(body, 'og:description') || extractMeta(body, 'description') || 'N/A';
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ title: title, description: description }));
});
}).on('error', function(e) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: e.message }));
});
});
server.listen(PORT, function() {
console.log('Proxy server running at http://localhost:' + PORT);
});
This server runs on port 3000. It receives the YouTube URL from the frontend, fetches the page HTML server-to-server, extracts the title and description from the meta tags, and returns clean JSON back to the frontend.
Step 3 — Update app.js
In app.js, find the direct fetch call:
/* ⚠️ Direct browser-to-YouTube call — triggers CORS error */
const res = await fetch(url);
const html = await res.text();
const title = html.match(/<title>([^<]*)<\/title>/i);
document.getElementById('title').textContent = title ? title[1].replace(' - YouTube', '') : 'N/A';
document.getElementById('author').textContent = 'N/A';
output.style.display = 'block';
Replace it with:
/* ✅ Calls the local Node.js proxy — not YouTube directly */
const res = await fetch('http://localhost:3000/fetch?url=' + encodeURIComponent(url));
const data = await res.json();
document.getElementById('title').textContent = data.title || 'N/A';
document.getElementById('author').textContent = data.description || 'N/A';
output.style.display = 'block';
These also remove the lines that tried to parse the raw HTML; they are no longer needed since the proxy returns clean JSON.
Step 4 — Start the proxy server
Open a new terminal in the project folder and run:
node server.js
You should see:

Leave this terminal running. Keep Live Server running in VS Code at the same time.
Step 5 — Test it
Go back to your browser at http://127.0.0.1:5500, paste a YouTube URL, and click Fetch Info.
The "Failed to Fetch" error is gone. The request now flows from the browser to your own proxy server at localhost:3000, which fetches YouTube server-to-server and returns the video title and description to the browser.
Summary
In this tutorial, you have learnt why HTTP requests made directly from the browser fail with a "Failed to Fetch" error — and how to fix it using the proxy pattern.
The key takeaway is this: browsers enforce the Same-Origin Policy, which blocks cross-origin requests to servers that don't return the right CORS headers. The proxy pattern solves this by routing your requests through your own backend — no browser restrictions, no exposed secrets.
This pattern works for any external server, YouTube, payment APIs, AI platforms, or anything else your frontend needs to talk to securely.
While this tutorial uses a YouTube URL fetch to demonstrate the error, the same proxy pattern applies when calling any external API — whether it's an AI platform like Gemini or OpenAI, a payment API like Stripe, or any other service that blocks direct browser requests. The only difference is that, with APIs, your proxy also handles authentication by securely storing the API key in environment variables on the server, keeping it out of the browser entirely.
Connect with me on my socials if you have any questions or want to follow along with future tutorials.
Thank you for reading, and I look forward to seeing you next time!





Top comments (0)