Skip to content

Instantly share code, notes, and snippets.

@matchilling
Last active October 13, 2025 08:29
Show Gist options
  • Select an option

  • Save matchilling/959f82e72f70325590741c1acfcbfe3a to your computer and use it in GitHub Desktop.

Select an option

Save matchilling/959f82e72f70325590741c1acfcbfe3a to your computer and use it in GitHub Desktop.
Cloudfare HTTP Request Headers

Visitor location headers

Cloudflare provides several HTTP request headers that include information about the visitor's location, IP, and network characteristics. These are automatically added to requests sent to your origin server (your backend) when traffic is proxied through Cloudflare.

List of available location headers:

Header Example Description
cf-ipcity San Francisco City name of the visitor's IP address.
cf-ipcontinent US, EU Continent code (NA, EU, AS, etc.).
cf-ipcountry US, DE, IN Two-letter ISO 3166-1 alpha-2 country code of the visitor's location.
cf-iplatitude 37.7749 Latitude of the visitor's IP.
cf-iplongitude -122.4194 Longitude of the visitor's IP.
cf-postal-code 94107 ZIP or postal code of the visitor.
cf-region-code CA ISO subdivision code, e.g., CA for California.
cf-region California Region or state of the visitor's IP address.
cf-timezone America/Los_Angeles Timezone name based on the IP geolocation.

Note: Besides the ISO-3166-1 alpha-2 codes, Cloudflare uses the following special country codes:

  • XX: Used for clients without country code data.
  • T1: Used for clients using the Tor network.
package cat.encordats.cloudflare;
import lombok.Getter;
/**
* Cloudflare provides several HTTP request headers that include information about the visitor's location, IP, and
* network characteristics. These are automatically added to requests sent to your origin server (your backend) when
* traffic is proxied through Cloudflare.
* <p>
* Cloudflare <a href="https://gist.github.com/matchilling/959f82e72f70325590741c1acfcbfe3a">visitor location headers</a>.
*/
@Getter
public enum CloudflareHeader {
/**
* City name (e.g. "San Francisco").
*/
CITY("cf-ipcity"),
/**
* Continent code (e.g. "NA" for North America).
*/
CONTINENT("cf-ipcontinent"),
/**
* Country code (e.g. "US" for United States).
*/
COUNTRY("cf-ipcountry"),
/**
* Latitude (e.g. "37.7749").
*/
LATITUDE("cf-iplatitude"),
/**
* Longitude (e.g. "-122.4194").
*/
LONGITUDE("cf-iplongitude"),
/**
* Region name (e.g. "California").
*/
REGION("cf-region"),
/**
* Region code (e.g. "CA" for California).
*/
REGION_CODE("cf-region-code"),
/**
* Timezone (e.g. "America/Los_Angeles").
*/
TIMEZONE("cf-timezone"),
/**
* ZIP code (e.g. "94103").
*/
ZIP_CODE("cf-postal-code");
private final String headerName;
CloudflareHeader(String headerName) {
this.headerName = headerName;
}
}
package com.matchilling.cloudflare;
import jakarta.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* A parser for Cloudflare-specific HTTP headers that provide geolocation information about the client.
*
* <p>Cloudflare adds several headers to requests that pass through its network, which can be used to determine the
* geographical location of the client. This class provides methods to extract and parse these headers.</p>
*/
public class CloudflareHeaderParser {
private static final Function<String, String> TO_LOWER_CASE = String::toLowerCase;
/**
* Cloudflare always converts non-ASCII characters to UTF-8 (using hexadecimal character representation) in HTTP
* request and response header values. This applies to location headers added by the Add visitor location headers
* managed transform.
*
* @see <a href="https://developers.cloudflare.com/fundamentals/get-started/reference/http-request-headers/#cloudflare-added-headers">Cloudflare Documentation</a>
*/
private static final Function<String, String> TO_UTF8 = value -> new String(value.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
/**
* Get the city name from Cloudflare headers (e.g. "San Francisco").
*
* @param request The HTTP servlet request.
* @return The city name if present.
*/
public Optional<String> cityName(HttpServletRequest request) {
return Optional.ofNullable(request).map(this::headersFrom).flatMap(this::cityName);
}
/**
* Get the city name from Cloudflare headers (e.g. "San Francisco").
*
* @param headers The HTTP headers map.
* @return The city name if present.
*/
public Optional<String> cityName(Map<String, String> headers) {
return valueFrom(CloudflareHeader.CITY, headers).map(TO_UTF8);
}
/**
* Get the continent code from Cloudflare headers (e.g. "NA" for North America).
*
* @param request The HTTP servlet request.
* @return The continent code if present.
*/
public Optional<String> continentCode(HttpServletRequest request) {
return Optional.ofNullable(request).map(this::headersFrom).flatMap(this::continentCode);
}
/**
* Get the continent code from Cloudflare headers (e.g. "NA" for North America).
*
* @param headers The HTTP headers map.
* @return The continent code if present.
*/
public Optional<String> continentCode(Map<String, String> headers) {
return valueFrom(CloudflareHeader.CONTINENT, headers).map(TO_LOWER_CASE);
}
/**
* Get the country code from Cloudflare headers (e.g. "US" for United States).
*
* @param request The HTTP servlet request.
* @return The country code if present.
*/
public Optional<String> countryCode(HttpServletRequest request) {
return Optional.ofNullable(request).map(this::headersFrom).flatMap(this::countryCode);
}
/**
* Get the country code from Cloudflare headers (e.g. "US" for United States).
*
* @param headers The HTTP headers map.
* @return The country code if present.
*/
public Optional<String> countryCode(Map<String, String> headers) {
return valueFrom(CloudflareHeader.COUNTRY, headers).map(TO_LOWER_CASE);
}
/**
* Get the latitude from Cloudflare headers (e.g. "37.7749").
*
* @param request The HTTP servlet request.
* @return The latitude if present.
*/
public Optional<String> latitude(HttpServletRequest request) {
return Optional.ofNullable(request).map(this::headersFrom).flatMap(this::latitude);
}
/**
* Get the latitude from Cloudflare headers (e.g. "37.7749").
*
* @param headers The HTTP headers map.
* @return The latitude if present.
*/
public Optional<String> latitude(Map<String, String> headers) {
return valueFrom(CloudflareHeader.LATITUDE, headers);
}
/**
* Get the longitude from Cloudflare headers (e.g. "-122.4194").
*
* @param request The HTTP servlet request.
* @return The longitude if present.
*/
public Optional<String> longitude(HttpServletRequest request) {
return Optional.ofNullable(request).map(this::headersFrom).flatMap(this::longitude);
}
/**
* Get the longitude from Cloudflare headers (e.g. "-122.4194").
*
* @param headers The HTTP headers map.
* @return The longitude if present.
*/
public Optional<String> longitude(Map<String, String> headers) {
return valueFrom(CloudflareHeader.LONGITUDE, headers);
}
/**
* Get the region name from Cloudflare headers (e.g. "California").
*
* @param request The HTTP servlet request.
* @return The region name if present.
*/
public Optional<String> regionName(HttpServletRequest request) {
return Optional.ofNullable(request).map(this::headersFrom).flatMap(this::regionName);
}
/**
* Get the region name from Cloudflare headers (e.g. "California").
*
* @param headers The HTTP headers map.
* @return The region name if present.
*/
public Optional<String> regionName(Map<String, String> headers) {
return valueFrom(CloudflareHeader.REGION, headers).map(TO_UTF8);
}
/**
* Get the region code from Cloudflare headers (e.g. "CA" for California).
*
* @param request The HTTP servlet request.
* @return The region code if present.
*/
public Optional<String> regionCode(HttpServletRequest request) {
return Optional.ofNullable(request).map(this::headersFrom).flatMap(this::regionCode);
}
/**
* Get the region code from Cloudflare headers (e.g. "CA" for California).
*
* @param headers The HTTP headers map.
* @return The region code if present.
*/
public Optional<String> regionCode(Map<String, String> headers) {
return valueFrom(CloudflareHeader.REGION_CODE, headers).map(TO_LOWER_CASE);
}
/**
* Get the timezone from Cloudflare headers (e.g. "America/Los_Angeles").
*
* @param request The HTTP servlet request.
* @return The timezone if present.
*/
public Optional<String> timezone(HttpServletRequest request) {
return Optional.ofNullable(request).map(this::headersFrom).flatMap(this::timezone);
}
/**
* Get the timezone from Cloudflare headers (e.g. "America/Los_Angeles").
*
* @param headers The HTTP headers map.
* @return The timezone if present.
*/
public Optional<String> timezone(Map<String, String> headers) {
return valueFrom(CloudflareHeader.TIMEZONE, headers);
}
/**
* Get the zip code from Cloudflare headers (e.g. "94103").
*
* @param request The HTTP servlet request.
* @return The zip code if present.
*/
public Optional<String> zipCode(HttpServletRequest request) {
return Optional.ofNullable(request).map(this::headersFrom).flatMap(this::zipCode);
}
/**
* Get the zip code from Cloudflare headers (e.g. "94103").
*
* @param headers The HTTP headers map.
* @return The zip code if present.
*/
public Optional<String> zipCode(Map<String, String> headers) {
return valueFrom(CloudflareHeader.ZIP_CODE, headers).map(TO_LOWER_CASE);
}
private Map<String, String> headersFrom(HttpServletRequest request) {
return Collections.list(request.getHeaderNames()).stream().collect(Collectors.toMap(it -> it, request::getHeader));
}
private Optional<String> valueFrom(CloudflareHeader header, Map<String, String> headers) {
return Optional.ofNullable(headers.getOrDefault(header.getHeaderName(), null)).filter(it -> !it.isBlank());
}
}
package cat.encordats.cloudflare;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
class CloudflareHeaderParserTest {
private final CloudflareHeaderParser subject = new CloudflareHeaderParser();
@DisplayName("CloudflareHeaderParser::cityName")
@Nested
class CityNameTest {
static Stream<Arguments> values() {
return Stream.of(
Arguments.of("some-header", "", Optional.empty()),
Arguments.of(CloudflareHeader.CITY.getHeaderName(), " ", Optional.empty()),
Arguments.of(CloudflareHeader.CITY.getHeaderName(), "San Francisco", Optional.of("San Francisco")),
Arguments.of(CloudflareHeader.CITY.getHeaderName(), "S\u00c3\u00a3o Paulo", Optional.of("São Paulo")));
}
@ParameterizedTest(name = "should parse city header ({0}=\"{1}\") correctly")
@MethodSource("values")
void shouldParseCityHeaderCorrectly(String headerName, String headerValue, Optional<String> expected) {
// given:
var headers = Map.of(headerName, headerValue);
// when:
var actual = subject.cityName(headers);
// then:
Assertions.assertEquals(expected, actual);
}
}
@DisplayName("CloudflareHeaderParser::regionName")
@Nested
class RegionNameTest {
static Stream<Arguments> values() {
return Stream.of(
Arguments.of("some-header", "", Optional.empty()),
Arguments.of(CloudflareHeader.REGION.getHeaderName(), " ", Optional.empty()),
Arguments.of(CloudflareHeader.REGION.getHeaderName(), "California", Optional.of("California")),
Arguments.of(CloudflareHeader.REGION.getHeaderName(), "S\u00c3\u00a3o Paulo", Optional.of("São Paulo")));
}
@ParameterizedTest(name = "should parse region name header ({0}=\"{1}\") correctly")
@MethodSource("values")
void shouldParseRegionNameHeaderCorrectly(String headerName, String headerValue, Optional<String> expected) {
// given:
var headers = Map.of(headerName, headerValue);
// when:
var actual = subject.regionName(headers);
// then:
Assertions.assertEquals(expected, actual);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment