Error Responses

This document describes the error response format and common error codes used by the Geog API.

Response Format

All error responses follow a consistent JSON format:

{
	"success": false,
	"error": "Error Type",
	"message": "Human-readable error description",
	"code": "MACHINE_READABLE_CODE"
}
FieldTypeDescription
successbooleanAlways false for errors
errorstringShort error type or title
messagestringDetailed, human-readable explanation
codestringMachine-readable error code for programmatic handling

HTTP Status Codes

2xx Success

CodeStatusDescription
200OKRequest successful
204No ContentRequest successful, no response body (e.g., empty tile)

4xx Client Errors

CodeStatusDescription
400Bad RequestInvalid request parameters or body
401UnauthorizedMissing or invalid authentication
403ForbiddenInsufficient permissions
404Not FoundResource does not exist
429Too Many RequestsRate limit exceeded

5xx Server Errors

CodeStatusDescription
500Internal Server ErrorUnexpected server error
502Bad GatewayUpstream service failure (e.g., JWKS fetch)
503Service UnavailableService temporarily unavailable

Error Codes Reference

Authentication Errors

AUTH_REQUIRED

Status: 401 Unauthorized

Cause: Request lacks authentication credentials.

Response:

{
	"success": false,
	"error": "Unauthorized",
	"message": "Missing or invalid authorization header",
	"code": "AUTH_REQUIRED"
}

Resolution: Include a valid Bearer token in the Authorization header.


INVALID_TOKEN

Status: 401 Unauthorized

Cause: JWT token is malformed or invalid.

Response:

{
	"success": false,
	"error": "Unauthorized",
	"message": "Invalid token format",
	"code": "INVALID_TOKEN"
}

Resolution: Ensure the token is a valid self-issued JWT.


TOKEN_EXPIRED

Status: 401 Unauthorized

Cause: JWT token has expired.

Response:

{
	"success": false,
	"error": "Unauthorized",
	"message": "Token has expired",
	"code": "TOKEN_EXPIRED"
}

Resolution: Refresh the token or re-authenticate.


INSUFFICIENT_SCOPE

Status: 403 Forbidden

Cause: Token lacks required permission scope.

Response:

{
	"success": false,
	"error": "Forbidden",
	"message": "Token does not have required scope: tiles:read",
	"code": "INSUFFICIENT_SCOPE"
}

Resolution: Request a token with the required scopes.


JWKS_FETCH_FAILED

Status: 502 Bad Gateway

Cause: Unable to fetch JWKS for token validation.

Response:

{
	"success": false,
	"error": "Bad Gateway",
	"message": "Failed to fetch JWKS",
	"code": "JWKS_FETCH_FAILED"
}

Resolution: Retry the request. Contact support if persistent.

Vector Tiles Errors

INVALID_EXTENSION

Status: 400 Bad Request

Cause: Tile request uses unsupported file extension.

Response:

{
	"success": false,
	"error": "Bad Request",
	"message": "Invalid tile extension. Must be 'mvt' or 'pbf'",
	"code": "INVALID_EXTENSION"
}

Resolution: Use .mvt or .pbf extension in tile requests.


TILE_NOT_FOUND

Status: 404 Not Found

Cause: Requested tile does not exist or is outside valid zoom range.

Response:

{
	"success": false,
	"error": "Not Found",
	"message": "Tile not found at z/x/y",
	"code": "TILE_NOT_FOUND"
}

Resolution: Verify tile coordinates are within valid range for the tileset.


TILESET_NOT_FOUND

Status: 404 Not Found

Cause: Requested tileset does not exist.

Response:

{
	"success": false,
	"error": "Not Found",
	"message": "Tileset 'unknown' not found",
	"code": "TILESET_NOT_FOUND"
}

Resolution: Verify tileset name is correct.

Rate Limiting Errors

RATE_LIMIT_EXCEEDED

Status: 429 Too Many Requests

Cause: Client has exceeded rate limit.

Response:

{
	"success": false,
	"error": "Too Many Requests",
	"message": "Rate limit exceeded. Please retry after 60 seconds.",
	"code": "RATE_LIMIT_EXCEEDED",
	"retryAfter": 60
}

Headers:

Retry-After: 60
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1699999999

Resolution: Wait for the rate limit window to reset. Implement exponential backoff.

Validation Errors

VALIDATION_ERROR

Status: 400 Bad Request

Cause: Request body failed validation.

Response:

{
	"success": false,
	"error": "Bad Request",
	"message": "Validation failed",
	"code": "VALIDATION_ERROR",
	"details": [
		{
			"field": "email",
			"message": "Must be a valid email address"
		}
	]
}

Resolution: Check the details array for specific field errors.

Client Implementation

Retry Wrapper with Error Handling

// Helper function for delays
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

// Token management (implement based on your auth system)
let token: string;
async function refreshToken() {
	/* ... */
}

async function apiRequest(url: string, options?: RequestInit, retryCount = 0) {
	const MAX_RETRIES = 3;

	const response = await fetch(url, {
		...options,
		headers: {
			Authorization: `Bearer ${token}`,
			'Content-Type': 'application/json',
			...options?.headers
		}
	});

	if (!response.ok) {
		const error = await response.json();

		switch (error.code) {
			case 'TOKEN_EXPIRED':
				// Refresh token and retry (with retry limit)
				if (retryCount < MAX_RETRIES) {
					await refreshToken();
					return apiRequest(url, options, retryCount + 1);
				}
				throw new ApiError(error);

			case 'RATE_LIMIT_EXCEEDED':
				// Wait and retry with backoff (with retry limit)
				if (retryCount < MAX_RETRIES) {
					await sleep(error.retryAfter * 1000);
					return apiRequest(url, options, retryCount + 1);
				}
				throw new ApiError(error);

			case 'VALIDATION_ERROR':
				// Handle validation errors
				throw new ValidationError(error.details);

			default:
				throw new ApiError(error);
		}
	}

	return response.json();
}

Logging Errors

Include the error code and message in logs for debugging:

try {
	await apiRequest('/v1/tiles/tileset/14/4680/6125.mvt');
} catch (error) {
	const { code, message } = error as ApiError;
	console.error(`API Error [${code}]: ${message}`);
}

User-Facing Messages

Map error codes to user-friendly messages:

const userMessages = {
	AUTH_REQUIRED: 'Please log in to continue.',
	TOKEN_EXPIRED: 'Your session has expired. Please log in again.',
	INSUFFICIENT_SCOPE: 'You do not have permission to access this resource.',
	RATE_LIMIT_EXCEEDED: 'Too many requests. Please wait a moment.'
};

Handling Rate Limits

Free Tier Limit Handling

async function fetchTile(url: string, token: string) {
	const response = await fetch(url, {
		headers: { Authorization: `Bearer ${token}` }
	});

	if (response.status === 429) {
		const data = await response.json();
		const resetDate = new Date(data.reset);

		// Show user when limits reset
		console.warn(`Rate limit exceeded. Resets at ${resetDate.toLocaleString()}`);

		// Consider upgrading
		showUpgradePrompt();
		return null;
	}

	return response;
}

Tips for free tier limits:

  1. Monitor rate limit headers: Check X-RateLimit-<Type>-Remaining before making requests
  2. Cache aggressively: Free tier limits are generous but caching reduces requests
  3. Query usage endpoint: Monitor your usage throughout the day to avoid surprises

Exponential Backoff

// Helper function for delays
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

async function fetchWithRetry(url: string, retries = 3) {
	for (let i = 0; i < retries; i++) {
		const response = await fetch(url);
		if (response.status === 429) {
			const retryAfter = parseInt(response.headers.get('Retry-After') || '60', 10);
			await sleep(retryAfter * 1000 * Math.pow(2, i));
			continue;
		}
		return response;
	}
	throw new Error('Rate limit exceeded after retries');
}

Optimizing Usage

  1. Cache responses: Vector tiles are cacheable for 24 hours. Implement client-side caching.
  2. Use appropriate zoom levels: Request only the zoom levels needed for your use case.
  3. Implement request deduplication: Avoid duplicate requests for the same resource.

See Also