Crosstalk-Solutions/project-nomad · warning · Error

Invalid PMTiles file URL

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.

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.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


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

Appendix: 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)