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.
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
| What | Why |
|---|---|
| A German proxy | The exit the site sees; ip-api.com confirms its country and timezone. |
navigator.geolocation and navigator.permissions | What a page reads when it asks where you are. |
Intl.DateTimeFormat().resolvedOptions().timeZone, navigator.language | The clock and language the browser reports. |
newContext({ permissions: ["geolocation"], geolocation, timezoneId, locale }) | Per-context overrides for all of them — no relaunch. |
How it works
- Launch Chromium through a German proxy and look up the exit country and timezone.
- In a default context, read geolocation, its permission, timezone and language.
- Repeat with the geolocation permission granted but no position set.
- Repeat with Berlin coordinates,
Europe/Berlinandde-DE.
The code
The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:
CDPFLEET_API_KEY— your API key (dashboard → API keys)PROXY_URL— your proxy, e.g.http://user:[email protected]:8000PROXY_URL_DE— a proxy exit in Germany
// 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();
}
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY, PROXY_URL_DE (a German proxy)
import json
import os
import requests
from playwright.sync_api import sync_playwright
KEY = os.environ["CDPFLEET_API_KEY"]
BERLIN = {"latitude": 52.52, "longitude": 13.405}
# What a page can learn about where the visitor is, from JavaScript.
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() };
}"""
res = requests.post("https://starter.cdpfleet.com/chromium/session",
headers={"x-api-key": KEY},
json={"proxy": os.environ["PROXY_URL_DE"], "headless": "new"}, timeout=60)
if not res.ok:
raise SystemExit(f"launch {res.status_code} {res.text}")
with sync_playwright() as p:
browser = p.chromium.connect(res.json()["wsUrl"], headers={"x-api-key": KEY})
try:
out = {"ip": None, "contexts": []}
setups = [
("defaults", {}),
("permission granted, no position", {"permissions": ["geolocation"]}),
("Berlin: geolocation + timezone + locale", {"permissions": ["geolocation"], "geolocation": BERLIN, "timezone_id": "Europe/Berlin", "locale": "de-DE"}),
]
for label, options in setups:
ctx = browser.new_context(**options)
page = ctx.new_page()
if out["ip"] is None:
page.goto("http://ip-api.com/json/?fields=query,countryCode,timezone", timeout=60000)
ip = json.loads(page.text_content("body"))
out["ip"] = {"country": ip["countryCode"], "timezone": ip["timezone"]}
page.goto("https://httpbin.org/html", timeout=60000)
out["contexts"].append({"setup": label, **page.evaluate(f"({PROBE})()")})
ctx.close()
print(json.dumps(out, indent=2, ensure_ascii=False))
finally:
browser.close()
// Maven: com.microsoft.playwright:playwright:1.60.0, com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1. env: CDPFLEET_API_KEY, PROXY_URL_DE (a German proxy)
import com.google.gson.*;
import com.microsoft.playwright.*;
import java.net.URI;
import java.net.http.*;
import java.util.*;
public class Main {
static final String KEY = System.getenv("CDPFLEET_API_KEY");
static final Gson GSON = new GsonBuilder().serializeNulls().create();
// What a page can learn about where the visitor is, from JavaScript.
static final String 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() };
}""";
public static void main(String[] args) throws Exception {
String body = "{\"proxy\": " + GSON.toJson(System.getenv("PROXY_URL_DE")) + ", \"headless\": \"new\"}";
HttpResponse<String> res = HttpClient.newHttpClient().send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/chromium/session"))
.header("x-api-key", KEY).header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body)).build(), HttpResponse.BodyHandlers.ofString());
if (res.statusCode() != 200) throw new RuntimeException("launch: " + res.statusCode() + " " + res.body());
String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().connect(wsUrl, new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
try {
JsonObject out = new JsonObject();
out.add("ip", JsonNull.INSTANCE);
JsonArray contexts = new JsonArray();
out.add("contexts", contexts);
Map<String, Browser.NewContextOptions> setups = new LinkedHashMap<>();
setups.put("defaults", new Browser.NewContextOptions());
setups.put("permission granted, no position", new Browser.NewContextOptions().setPermissions(List.of("geolocation")));
setups.put("Berlin: geolocation + timezone + locale", new Browser.NewContextOptions().setPermissions(List.of("geolocation"))
.setGeolocation(52.52, 13.405).setTimezoneId("Europe/Berlin").setLocale("de-DE"));
for (Map.Entry<String, Browser.NewContextOptions> setup : setups.entrySet()) {
BrowserContext ctx = browser.newContext(setup.getValue());
Page page = ctx.newPage();
if (out.get("ip").isJsonNull()) {
page.navigate("http://ip-api.com/json/?fields=query,countryCode,timezone", new Page.NavigateOptions().setTimeout(60000));
JsonObject ip = JsonParser.parseString(page.textContent("body")).getAsJsonObject();
JsonObject where = new JsonObject();
where.add("country", ip.get("countryCode"));
where.add("timezone", ip.get("timezone"));
out.add("ip", where);
}
page.navigate("https://httpbin.org/html", new Page.NavigateOptions().setTimeout(60000));
JsonObject entry = new JsonObject();
entry.addProperty("setup", setup.getKey());
for (Map.Entry<String, JsonElement> e : GSON.toJsonTree(page.evaluate("(" + PROBE + ")()")).getAsJsonObject().entrySet()) entry.add(e.getKey(), e.getValue());
contexts.add(entry);
ctx.close();
}
System.out.println(new GsonBuilder().serializeNulls().setPrettyPrinting().disableHtmlEscaping().create().toJson(out));
} finally {
browser.close();
}
}
}
}
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL_DE (a German proxy)
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;
// What a page can learn about where the visitor is, from JavaScript.
const string 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() };
}
""";
var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
var res = await http.PostAsJsonAsync("https://starter.cdpfleet.com/chromium/session",
new { proxy = Environment.GetEnvironmentVariable("PROXY_URL_DE"), headless = "new" });
if (!res.IsSuccessStatusCode) throw new Exception($"launch: {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;
using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectAsync(wsUrl, new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });
try
{
JsonObject? ip = null;
var contexts = new JsonArray();
var setups = new (string Label, BrowserNewContextOptions Options)[]
{
("defaults", new()),
("permission granted, no position", new() { Permissions = ["geolocation"] }),
("Berlin: geolocation + timezone + locale", new()
{
Permissions = ["geolocation"],
Geolocation = new() { Latitude = 52.52f, Longitude = 13.405f },
TimezoneId = "Europe/Berlin",
Locale = "de-DE",
}),
};
foreach (var (label, options) in setups)
{
var ctx = await browser.NewContextAsync(options);
var page = await ctx.NewPageAsync();
if (ip is null)
{
await page.GotoAsync("http://ip-api.com/json/?fields=query,countryCode,timezone", new() { Timeout = 60000 });
var info = JsonNode.Parse((await page.TextContentAsync("body"))!)!;
ip = new JsonObject { ["country"] = info["countryCode"]?.DeepClone(), ["timezone"] = info["timezone"]?.DeepClone() };
}
await page.GotoAsync("https://httpbin.org/html", new() { Timeout = 60000 });
var entry = new JsonObject { ["setup"] = label };
foreach (var prop in ((JsonObject)Plain(await page.EvaluateAsync<JsonElement>($"({Probe})()"))!).ToList())
entry[prop.Key] = prop.Value?.DeepClone();
contexts.Add(entry);
await ctx.CloseAsync();
}
var result = new JsonObject { ["ip"] = ip, ["contexts"] = contexts };
Console.WriteLine(result.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
}
finally
{
await browser.CloseAsync();
}
// EvaluateAsync<JsonElement> tags every object with a "$id" reference key; drop those.
static JsonNode? Plain(JsonElement e) => e.ValueKind switch
{
JsonValueKind.Object => new JsonObject(e.EnumerateObject().Where(p => p.Name != "$id")
.Select(p => KeyValuePair.Create(p.Name, Plain(p.Value)))),
JsonValueKind.Array => new JsonArray(e.EnumerateArray().Select(Plain).ToArray()),
_ => JsonNode.Parse(e.GetRawText()),
};
// go get github.com/playwright-community/[email protected]
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY, PROXY_URL_DE (a German proxy)
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"github.com/playwright-community/playwright-go"
)
var key = os.Getenv("CDPFLEET_API_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() };
}`
func launch(name string, options map[string]any) (map[string]any, error) {
body, _ := json.Marshal(options)
req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/"+name+"/session", bytes.NewReader(body))
req.Header.Set("x-api-key", key)
req.Header.Set("content-type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
msg, _ := io.ReadAll(res.Body)
return nil, fmt.Errorf("launch %s: %s %s", name, res.Status, msg)
}
var session map[string]any
return session, json.NewDecoder(res.Body).Decode(&session)
}
type ipInfo struct {
Country string `json:"country"`
Timezone string `json:"timezone"`
}
type contextResult struct {
Setup string `json:"setup"`
Geolocation any `json:"geolocation"`
Permission any `json:"permission"`
Timezone any `json:"timezone"`
Language any `json:"language"`
UTCOffsetMin any `json:"utc_offset_min"`
}
type output struct {
IP *ipInfo `json:"ip"`
Contexts []contextResult `json:"contexts"`
}
type setup struct {
label string
options playwright.BrowserNewContextOptions
}
func run(browser playwright.Browser) (output, error) {
out := output{Contexts: []contextResult{}}
setups := []setup{
{"defaults", playwright.BrowserNewContextOptions{}},
{"permission granted, no position", playwright.BrowserNewContextOptions{Permissions: []string{"geolocation"}}},
{"Berlin: geolocation + timezone + locale", playwright.BrowserNewContextOptions{
Permissions: []string{"geolocation"},
Geolocation: &playwright.Geolocation{Latitude: 52.52, Longitude: 13.405},
TimezoneId: playwright.String("Europe/Berlin"),
Locale: playwright.String("de-DE"),
}},
}
for _, s := range setups {
ctx, err := browser.NewContext(s.options)
if err != nil {
return out, err
}
page, err := ctx.NewPage()
if err != nil {
return out, err
}
if out.IP == nil {
if _, err := page.Goto("http://ip-api.com/json/?fields=query,countryCode,timezone", playwright.PageGotoOptions{Timeout: playwright.Float(60000)}); err != nil {
return out, err
}
text, err := page.TextContent("body")
if err != nil {
return out, err
}
var ip struct {
CountryCode string `json:"countryCode"`
Timezone string `json:"timezone"`
}
if err := json.Unmarshal([]byte(text), &ip); err != nil {
return out, err
}
out.IP = &ipInfo{Country: ip.CountryCode, Timezone: ip.Timezone}
}
if _, err := page.Goto("https://httpbin.org/html", playwright.PageGotoOptions{Timeout: playwright.Float(60000)}); err != nil {
return out, err
}
v, err := page.Evaluate("(" + probe + ")()")
if err != nil {
return out, err
}
m := v.(map[string]any)
out.Contexts = append(out.Contexts, contextResult{
Setup: s.label,
Geolocation: m["geolocation"],
Permission: m["permission"],
Timezone: m["timezone"],
Language: m["language"],
UTCOffsetMin: m["utc_offset_min"],
})
if err := ctx.Close(); err != nil {
return out, err
}
}
return out, nil
}
func main() {
session, err := launch("chromium", map[string]any{"proxy": os.Getenv("PROXY_URL_DE"), "headless": "new"})
if err != nil {
log.Fatal(err)
}
pw, err := playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true})
if err != nil {
log.Fatal(err)
}
defer pw.Stop()
browser, err := pw.Chromium.Connect(session["wsUrl"].(string), playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}})
if err != nil {
log.Fatal(err)
}
out, err := run(browser)
browser.Close()
if err != nil {
log.Fatal(err)
}
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
enc.Encode(out)
fmt.Print(buf.String())
}
What we got
| Context | Geolocation | Permission | Timezone | Language |
|---|---|---|---|---|
| defaults | permission denied | prompt | UTC | en-US |
| permission granted, no position | Timeout expired | granted | UTC | en-US |
| Berlin: geolocation + timezone + locale | 52.52, 13.41 | granted | Europe/Berlin | de-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
- Defaults contradict the proxy: the exit was German (
Europe/Berlin) but the browser said UTC anden-US, and refused geolocation. - Granting the permission isn't enough: without a position,
getCurrentPositionjust times out — the page sees a permission it can't use. - Three options make it consistent:
geolocation,timezoneIdandlocaleon the context, plus thegeolocationpermission — the page then sees Berlin,Europe/Berlin(UTC+2) andde-DE. - Match them to the exit, per context: look up your proxy's country first (or use a country-pinned residential token); Camoufox can do this for you with
geoip: true.