Crosstalk-Solutions/project-nomad · warning · Error
Invalid PMTiles file URL: ${url}. URL must end with .pmtiles
Error message
Invalid PMTiles file URL: ${url}. URL must end with .pmtiles What it means
Input validation in MapService.downloadRemote: the provided URL is parsed with new URL() and its pathname must end with the .pmtiles extension before a remote download job is created. Any other extension (or no extension) is rejected outright.
Source
Thrown at admin/app/services/map_service.ts:261
)
} catch (err) {
logger.warn(`[MapService] Failed to remove superseded file ${decision.path}:`, err)
}
} else if (decision.reason !== 'first_install' && decision.reason !== 'same_file') {
logger.info(
`[MapService] Kept prior ${parsed.resource_id} file (reason: ${decision.reason})`
)
}
} catch (error) {
logger.error(`[MapService] Failed to create InstalledResource for ${filename}:`, error)
}
}
}
async downloadRemote(url: string): Promise<{ filename: string; jobId?: string }> {
const parsed = new URL(url)
if (!parsed.pathname.endsWith('.pmtiles')) {
throw new Error(`Invalid PMTiles file URL: ${url}. URL must end with .pmtiles`)
}
const existing = await RunDownloadJob.getActiveByUrl(url)
if (existing) {
throw new Error(`Download already in progress for URL ${url}`)
}
const filename = url.split('/').pop()
if (!filename) {
throw new Error('Could not determine filename from URL')
}
const filepath = join(process.cwd(), this.mapStoragePath, 'pmtiles', filename)
// First, ensure base assets are present - regions depend on them
const baseAssetsExist = await this.ensureBaseAssets()
if (!baseAssetsExist) {View on GitHub (pinned to 0bd1c6f4f9)
Solutions
- Point at the actual PMTiles archive URL ending in .pmtiles
- If the provider uses extensionless URLs, download/rename the file to end with .pmtiles and host it accordingly
- Normalize case before calling if uppercase extensions must be supported
Example fix
// before
await mapService.downloadRemote('https://tiles.example.com/style.json')
// after
await mapService.downloadRemote('https://tiles.example.com/osm.pmtiles') Defensive patterns
Strategy: validation
Validate before calling
let u: URL
try { u = new URL(url) } catch { throw new BadRequestException('Invalid URL') }
if (!u.pathname.toLowerCase().endsWith('.pmtiles'))
throw new BadRequestException('URL must point to a .pmtiles file')
await mapService.downloadRemote(url) Type guard
const isPmtilesUrl = (url: string): boolean => {
try { return new URL(url).pathname.toLowerCase().endsWith('.pmtiles') }
catch { return false }
} Try / catch
try {
await mapService.downloadRemote(url)
} catch (e) {
if ((e as Error).message.includes('must end with .pmtiles'))
return res.status(400).json({ error: 'Provide a direct .pmtiles archive URL' })
throw e
} Prevention
- Validate URL shape in the form/UI before submission
- Document that tile JSON/style URLs are not accepted — only PMTiles archives
- Trim whitespace and stray query fragments from pasted URLs
When it happens
Trigger: downloadRemote('https://host/tiles/manifest.json'), URLs with query strings where the extension check should be on pathname (query is fine), URLs ending in .pmtiles.gz or trailing slashes, uppercase .PMTILES on case-sensitive comparison.
Common situations: Users pasting a tile JSON or style URL instead of the PMTiles archive URL, URLs from providers that serve tiles under extensionless paths, uppercase extensions from Windows-authored configs.
Related errors
- Could not determine filename from URL
- Filename is required
- Invalid document slug
- Download already in progress for URL ${url}
- Base map assets are missing and could not be downloaded. Ple
AI-assisted analysis of Crosstalk-Solutions/project-nomad@0bd1c6f4f9 (2026-08-27).
Data as JSON: /api/errors/d9cdc7a4011b0613.
Report an issue: GitHub.