← Back to Help Center

🔒 Secure Proxy Setup Guide

Learn how to set up a secure, serverless proxy to enable OpenFIGI and Yahoo Finance integration without CORS errors.

Why is this needed?
Some APIs (like OpenFIGI and Yahoo Finance) block requests directly from web browsers due to CORS (Cross-Origin Resource Sharing) security policies. This proxy acts as a secure middleman to bypass these restrictions while keeping your API keys safe.

🛠️ Prerequisites

🚀 Step-by-Step Setup

1. Create a Cloudflare Worker

  1. Log in to your Cloudflare Dashboard.
  2. Go to Workers & Pages on the left sidebar.
  3. Click Create ApplicationCreate Worker.
  4. Select "Start with Hello World!" (this creates a basic worker we can edit).
  5. Name it tradepro-proxy (or similar) and click Deploy.
  6. Click Edit Code.

2. Deploy the Proxy Code

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.

3. Configure TradePro

  1. Copy your Worker URL (e.g., https://tradepro-proxy.your-name.workers.dev).
  2. Open TradePro Portfolio Manager.
  3. Click on the 📊 API Configuration tab.
  4. Scroll to the OpenFIGI section.
  5. Paste your URL into the Proxy URL field.
  6. Click Save.

✅ You are now ready to fetch data securely!

4. Troubleshooting

Common Issue: 404 Not Found on /yahoo
If you see a "Proxy Update Required" notification or 404 errors in the console when fetching chart data:
  • This means your deployed worker is outdated.
  • Solution: Go back to Cloudflare Dashboard, edit your worker, and paste the latest code from step 2 above.
  • Make sure to click Save and Deploy.