Home About Me

Turning EdgeOne Pages Functions Into a Free Geolocation API

Getting a visitor's location is one of those small features that quietly powers a lot of product behavior. A site may switch languages based on country, show prices in a nearby currency, tag analytics with region, or serve different content to different areas. Usually that means querying an IP geolocation service. EdgeOne Pages Functions offer another route: the runtime already exposes network and location metadata, so a few lines of JavaScript can turn it into a JSON endpoint.

The resulting tool is EdgeOne-Pages-Functions-Geo. A live demo is available at api-geo.tianhw.top; it runs on EdgeOne's overseas version.

What the function returns

The function runs on Tencent Cloud's global edge nodes. It returns the visitor's public IP address along with country and region details, province or state, city, ASN, latitude and longitude, and ISP information. EdgeOne also provides a monthly free request allowance, and the CPU cost of this kind of function is essentially negligible. Because the response includes CORS headers, front-end JavaScript can call it directly with fetch, and the payload is standard JSON.

How it works

Traditional IP geolocation requires maintaining a large IP database. In EdgeOne Pages Functions, much of that work is handled by the runtime. The relevant data is available on context.request.eo: request.eo.clientIp holds the client IP, and request.eo.geo holds the location and network fields. The function simply extracts those values, supplies fallbacks where needed, and returns them as JSON.

The code also handles CORS. An OPTIONS request gets a 204 response with the appropriate access-control headers; other requests receive the JSON payload with the same CORS policy. If eo.clientIp is not present, the function checks eo-client-ip, x-real-ip, cf-connecting-ip, and the first value in x-forwarded-for before falling back to 127.0.0.1.

export function onRequest(context) {
  const { request } = context;

  // Extract metadata provided by EdgeOne
  const eo = request.eo || {};
  const geo = eo.geo || {};

  // Based on debugging information, the IP is stored in eo.clientIp
  const ip = eo.clientIp ||
    request.headers.get('eo-client-ip') ||
    request.headers.get('x-real-ip') ||
    request.headers.get('cf-connecting-ip') ||
    request.headers.get('x-forwarded-for')?.split(',')[0].trim() ||
    '127.0.0.1';

  const responseData = {
    ip: ip,
    asn: geo.asn || null,
    countryName: geo.countryName || 'unknown',
    countryCodeAlpha2: geo.countryCodeAlpha2 || 'unknown',
    countryCodeAlpha3: geo.countryCodeAlpha3 || 'unknown',
    countryCodeNumeric: geo.countryCodeNumeric || 'unknown',
    regionName: geo.regionName || 'unknown',
    regionCode: geo.regionCode || 'unknown',
    cityName: geo.cityName || 'unknown',
    latitude: geo.latitude || null,
    longitude: geo.longitude || null,
    cisp: geo.cisp || 'unknown'
  };

  // Handle OPTIONS requests (CORS preflight)
  if (request.method === 'OPTIONS') {
    return new Response(null, {
      status: 204,
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization',
        'Access-Control-Max-Age': '86400',
      },
    });
  }

  // Return geolocation information and set CORS headers
  return new Response(JSON.stringify(responseData), {
    headers: {
      'Content-Type': 'application/json;charset=UTF-8',
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    },
  });
}

Response fields

The endpoint returns JSON with the visitor's IP address, location, and network details.

<table> <thead> <tr> <th>Field</th> <th>Type</th> <th>Meaning</th> </tr> </thead> <tbody> <tr> <td>ip</td> <td>String</td> <td>Visitor's public IP address</td> </tr> <tr> <td>asn</td> <td>Number</td> <td>Autonomous System Number</td> </tr> <tr> <td>countryName</td> <td>String</td> <td>Country name in English</td> </tr> <tr> <td>countryCodeAlpha2</td> <td>String</td> <td>ISO two-letter country code (for example, CN)</td> </tr> <tr> <td>countryCodeAlpha3</td> <td>String</td> <td>ISO three-letter country code (for example, CHN)</td> </tr> <tr> <td>countryCodeNumeric</td> <td>String</td> <td>ISO numeric country code</td> </tr> <tr> <td>regionName</td> <td>String</td> <td>Province or autonomous region name</td> </tr> <tr> <td>regionCode</td> <td>String</td> <td>Region code (for example, CN-SD)</td> </tr> <tr> <td>cityName</td> <td>String</td> <td>City name</td> </tr> <tr> <td>latitude</td> <td>Number</td> <td>Latitude</td> </tr> <tr> <td>longitude</td> <td>Number</td> <td>Longitude</td> </tr> <tr> <td>cisp</td> <td>String</td> <td>ISP name</td> </tr> </tbody> </table>

A sample response looks like this:

{
  "ip": "45.221.98.10",
  "asn": 140869,
  "countryName": "United States of America",
  "countryCodeAlpha2": "US",
  "countryCodeAlpha3": "USA",
  "countryCodeNumeric": "840",
  "regionName": "California",
  "regionCode": "US-CA",
  "cityName": "Los Angeles",
  "latitude": 34.05223083496094,
  "longitude": -118.24368286132812,
  "cisp": "Turing Group Limited"
}

For projects that only need a lightweight location lookup, this keeps the implementation small: one function, no separate IP database, and a response that can be consumed by a browser or a server-side client.