cdpfleet Docs GitHub Dashboard

Cases / #35 · 2026-10-11 · Easy

A German IP with a UTC clock: making location, timezone and permissions agree

What a page learns about where you are — geolocation, its permission, timezone and language — through a German proxy, with defaults and with a Berlin context. Defaults contradict the IP; three context options fix it.

Chromium

Run on production on 2026-10-11: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go

The problem

A proxy changes where your traffic comes from, not what the browser says about itself. Sites compare the IP's country with the browser's timezone, language and — if they ask — its geolocation. A German IP with a UTC clock, en-US and a refused location prompt is an easy inconsistency to flag. What does the browser report by default, and what does it take to make it agree with the proxy?

What we used, and why

WhatWhy
A German proxyThe exit the site sees; ip-api.com confirms its country and timezone.
navigator.geolocation and navigator.permissionsWhat a page reads when it asks where you are.
Intl.DateTimeFormat().resolvedOptions().timeZone, navigator.languageThe clock and language the browser reports.
newContext({ permissions: ["geolocation"], geolocation, timezoneId, locale })Per-context overrides for all of them — no relaunch.

How it works

  1. Launch Chromium through a German proxy and look up the exit country and timezone.
  2. In a default context, read geolocation, its permission, timezone and language.
  3. Repeat with the geolocation permission granted but no position set.
  4. Repeat with Berlin coordinates, Europe/Berlin and de-DE.

The code

The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:

// npm install [email protected]
// env: CDPFLEET_API_KEY, PROXY_URL_DE (a German proxy)
import { chromium } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;
const BERLIN = { latitude: 52.52, longitude: 13.405 };

const res = await fetch('https://starter.cdpfleet.com/chromium/session', {
  method: 'POST',
  headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
  body: JSON.stringify({ proxy: process.env.PROXY_URL_DE, headless: 'new' }),
});
if (!res.ok) throw new Error(`launch ${res.status} ${await res.text()}`);
const { wsUrl } = await res.json();
const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });

// What a page can learn about where the visitor is, from JavaScript.
const PROBE = `async () => {
  const geo = await new Promise((resolve) => navigator.geolocation.getCurrentPosition(
    (p) => resolve({ lat: Math.round(p.coords.latitude * 100) / 100, lon: Math.round(p.coords.longitude * 100) / 100 }),
    (e) => resolve({ error: e.code === 1 ? 'permission denied' : e.message }),
    { timeout: 5000 }));
  const perm = (await navigator.permissions.query({ name: 'geolocation' })).state;
  return { geolocation: geo, permission: perm, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, language: navigator.language, utc_offset_min: -new Date().getTimezoneOffset() };
}`;

try {
  const out = { ip: null, contexts: [] };
  const setups = [
    ['defaults', {}],
    ['permission granted, no position', { permissions: ['geolocation'] }],
    ['Berlin: geolocation + timezone + locale', { permissions: ['geolocation'], geolocation: BERLIN, timezoneId: 'Europe/Berlin', locale: 'de-DE' }],
  ];
  for (const [label, options] of setups) {
    const ctx = await browser.newContext(options);
    const page = await ctx.newPage();
    if (!out.ip) {
      await page.goto('http://ip-api.com/json/?fields=query,countryCode,timezone', { timeout: 60000 });
      const ip = JSON.parse(await page.textContent('body'));
      out.ip = { country: ip.countryCode, timezone: ip.timezone };
    }
    await page.goto('https://httpbin.org/html', { timeout: 60000 });
    out.contexts.push({ setup: label, ...(await page.evaluate(`(${PROBE})()`)) });
    await ctx.close();
  }
  console.log(JSON.stringify(out, null, 2));
} finally {
  await browser.close();
}

What we got

ContextGeolocationPermissionTimezoneLanguage
defaultspermission deniedpromptUTCen-US
permission granted, no positionTimeout expiredgrantedUTCen-US
Berlin: geolocation + timezone + locale52.52, 13.41grantedEurope/Berlinde-DE

From the Node.js run on 2026-10-11. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.

Raw output (Node.js)
{
  "ip": {
    "country": "DE",
    "timezone": "Europe/Berlin"
  },
  "contexts": [
    {
      "setup": "defaults",
      "geolocation": {
        "error": "permission denied"
      },
      "permission": "prompt",
      "timezone": "UTC",
      "language": "en-US",
      "utc_offset_min": 0
    },
    {
      "setup": "permission granted, no position",
      "geolocation": {
        "error": "Timeout expired"
      },
      "permission": "granted",
      "timezone": "UTC",
      "language": "en-US",
      "utc_offset_min": 0
    },
    {
      "setup": "Berlin: geolocation + timezone + locale",
      "geolocation": {
        "lat": 52.52,
        "lon": 13.41
      },
      "permission": "granted",
      "timezone": "Europe/Berlin",
      "language": "de-DE",
      "utc_offset_min": 120
    }
  ]
}

Takeaways