Learn how to set up a secure, serverless proxy to enable OpenFIGI and Yahoo Finance integration without CORS errors.
tradepro-proxy (or similar) and click Deploy.Delete the existing code in the editor and paste the following JavaScript code:
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const path = url.pathname;
// Common CORS headers
const corsHeaders = {
"Access-Control-Allow-Origin": "*", // In production, replace * with your specific domain
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, X-User-Api-Key",
};
// Handle Preflight Requests
if (request.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders });
}
// Route: OpenFIGI
if (path.endsWith("/openfigi")) {
return handleOpenFigi(request, corsHeaders);
}
// Route: Yahoo Finance
if (path.endsWith("/yahoo")) {
return handleYahooFinance(request, url.searchParams, corsHeaders);
}
// Route: EPA Envirofacts
if (path.endsWith("/epa")) {
return handleEPA(request, url.searchParams, corsHeaders);
}
// Route: IEX Cloud
if (path.endsWith("/iex")) {
return handleIEX(request, url.searchParams, corsHeaders);
}
// Default: Not Found
return new Response("Not Found", { status: 404, headers: corsHeaders });
},
};
/**
* Handles requests to OpenFIGI API
* Forwards POST requests to https://api.openfigi.com/v3/mapping
*/
async function handleOpenFigi(request, corsHeaders) {
if (request.method !== "POST") {
return new Response("Method Not Allowed", { status: 405, headers: corsHeaders });
}
const userApiKey = request.headers.get("X-User-Api-Key");
// Construct headers for OpenFIGI
const openFigiHeaders = {
"Content-Type": "application/json",
// Only inject key if provided by user
...(userApiKey && { "X-OPENFIGI-APIKEY": userApiKey }),
};
try {
const response = await fetch("https://api.openfigi.com/v3/mapping", {
method: "POST",
headers: openFigiHeaders,
body: request.body, // Forward the JSON body
});
const data = await response.text();
// Create new headers based on the response but with our CORS headers
const responseHeaders = new Headers(response.headers);
Object.keys(corsHeaders).forEach(key => responseHeaders.set(key, corsHeaders[key]));
return new Response(data, {
status: response.status,
headers: responseHeaders
});
} catch (error) {
return new Response(JSON.stringify({ error: "Proxy Error: " + error.message }), {
status: 500,
headers: { ...corsHeaders, "Content-Type": "application/json" }
});
}
}
/**
* Handles requests to Yahoo Finance API
* Implements Crumb & Cookie authentication to bypass 401 errors
*/
async function handleYahooFinance(request, searchParams, corsHeaders) {
if (request.method !== "GET") {
return new Response("Method Not Allowed", { status: 405, headers: corsHeaders });
}
const symbol = searchParams.get("symbol");
const modules = searchParams.get("modules");
const type = searchParams.get("type") || "quote"; // 'quote' or 'chart'
const range = searchParams.get("range");
const interval = searchParams.get("interval");
if (!symbol) {
return new Response("Missing symbol parameter", { status: 400, headers: corsHeaders });
}
try {
// 1. Get Cookie and Crumb
const authData = await getYahooAuth();
if (!authData) {
return new Response(JSON.stringify({
error: "Upstream Authentication Failed",
details: "Could not retrieve Cookie/Crumb from Yahoo Finance. The service may be blocking the request."
}), {
status: 502,
headers: { ...corsHeaders, "Content-Type": "application/json" }
});
}
const { cookie, crumb } = authData;
// 2. Construct URL based on type
let targetUrl;
if (type === 'chart') {
// Chart API: https://query1.finance.yahoo.com/v8/finance/chart/AAPL?range=1mo&interval=1d
targetUrl = `https://query1.finance.yahoo.com/v8/finance/chart/${symbol}?range=${range||'1mo'}&interval=${interval||'1d'}&crumb=${crumb}`;
} else {
// Quote Summary API
targetUrl = `https://query1.finance.yahoo.com/v10/finance/quoteSummary/${symbol}?modules=${modules || 'esgScores'}&crumb=${crumb}`;
}
// 3. Fetch Data with Cookie
const response = await fetch(targetUrl, {
method: "GET",
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Cookie": cookie
}
});
const data = await response.text();
if (!response.ok) {
console.log(`Yahoo Upstream Error (${response.status}) for ${symbol}:`, data);
}
const responseHeaders = new Headers(response.headers);
Object.keys(corsHeaders).forEach(key => responseHeaders.set(key, corsHeaders[key]));
// If upstream failed, return the error details to help debugging
if (!response.ok) {
return new Response(data, {
status: response.status,
headers: responseHeaders
});
}
return new Response(data, {
status: response.status,
headers: responseHeaders
});
} catch (error) {
return new Response(JSON.stringify({
error: "Proxy Error: " + error.message,
details: "Failed to authenticate with Yahoo Finance"
}), {
status: 500,
headers: { ...corsHeaders, "Content-Type": "application/json" }
});
}
}
/**
* Helper to get Yahoo Cookie and Crumb
*/
async function getYahooAuth() {
try {
// Step 1: Get Cookie from fc.yahoo.com
const cookieReq = await fetch("https://fc.yahoo.com", {
method: "GET",
redirect: "manual"
});
const setCookie = cookieReq.headers.get("set-cookie");
if (!setCookie) {
return null;
}
const cookie = setCookie.split(';')[0];
// Step 2: Get Crumb using the Cookie
const crumbReq = await fetch("https://query1.finance.yahoo.com/v1/test/getcrumb", {
method: "GET",
headers: {
"Cookie": cookie,
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
}
});
const crumb = await crumbReq.text();
if (!crumb || crumb.includes("Invalid Crumb") || crumb.includes("{")) {
console.log("Invalid crumb received:", crumb);
return null;
}
return { cookie, crumb };
} catch (e) {
console.log("Yahoo Auth Error:", e);
return null;
}
}
/**
* Handles requests to EPA Envirofacts API
* Forwards GET requests to https://enviro.epa.gov/enviro/efservice/{path}
* Expects 'path' as query parameter
*/
async function handleEPA(request, searchParams, corsHeaders) {
if (request.method !== "GET") {
return new Response("Method Not Allowed", { status: 405, headers: corsHeaders });
}
const path = searchParams.get("path");
if (!path) {
return new Response("Missing path parameter", { status: 400, headers: corsHeaders });
}
// Construct EPA URL
// Example: https://data.epa.gov/efservice/FRS_FACILITY_SITE/REGISTRY_ID/110000789012
const targetUrl = `https://data.epa.gov/efservice/${path}`;
// Append any other query parameters (except 'path')
const otherParams = new URLSearchParams(searchParams);
otherParams.delete("path");
const queryString = otherParams.toString();
const finalUrl = queryString ? `${targetUrl}?${queryString}` : targetUrl;
try {
const response = await fetch(finalUrl, {
method: "GET",
headers: {
"User-Agent": "TradePro-SwanScoring/1.0"
}
});
const data = await response.text();
const responseHeaders = new Headers(response.headers);
Object.keys(corsHeaders).forEach(key => responseHeaders.set(key, corsHeaders[key]));
return new Response(data, {
status: response.status,
headers: responseHeaders
});
} catch (error) {
return new Response(JSON.stringify({ error: "Proxy Error: " + error.message }), {
status: 500,
headers: { ...corsHeaders, "Content-Type": "application/json" }
});
}
}
/**
* Handles requests to IEX Cloud API
*/
async function handleIEX(request, searchParams, corsHeaders) {
if (request.method !== "GET") {
return new Response("Method Not Allowed", { status: 405, headers: corsHeaders });
}
const path = searchParams.get("path");
const token = searchParams.get("token");
const isSandbox = searchParams.get("sandbox") === "true";
if (!path || !token) {
return new Response("Missing path or token parameter", { status: 400, headers: corsHeaders });
}
// Reconstruct the query string (excluding 'path', 'token', 'sandbox')
const queryParams = new URLSearchParams();
queryParams.append("token", token);
for (const [key, value] of searchParams) {
if (key !== "path" && key !== "token" && key !== "sandbox") {
queryParams.append(key, value);
}
}
const queryString = queryParams.toString();
const baseUrl = isSandbox ? 'https://sandbox.iexapis.com/stable' : 'https://cloud.iexapis.com/stable';
const targetUrl = `${baseUrl}/${path}?${queryString}`;
try {
const response = await fetch(targetUrl, {
method: "GET"
});
const data = await response.text();
const responseHeaders = new Headers(response.headers);
Object.keys(corsHeaders).forEach(key => responseHeaders.set(key, corsHeaders[key]));
return new Response(data, {
status: response.status,
headers: responseHeaders
});
} catch (error) {
return new Response(JSON.stringify({ error: "Proxy Error: " + error.message }), {
status: 500,
headers: { ...corsHeaders, "Content-Type": "application/json" }
});
}
}
Click Save and Deploy in the top right corner.
https://tradepro-proxy.your-name.workers.dev).✅ You are now ready to fetch data securely!