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

  1. Point at the actual PMTiles archive URL ending in .pmtiles
  2. If the provider uses extensionless URLs, download/rename the file to end with .pmtiles and host it accordingly
  3. 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

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


AI-assisted analysis of Crosstalk-Solutions/project-nomad@0bd1c6f4f9 (2026-08-27). Data as JSON: /api/errors/d9cdc7a4011b0613. Report an issue: GitHub.