{"record":{"id":"d9cdc7a4011b0613","repo":"Crosstalk-Solutions/project-nomad","slug":"invalid-pmtiles-file-url-url-url-must-end-wit","errorCode":null,"errorMessage":"Invalid PMTiles file URL: ${url}. URL must end with .pmtiles","messagePattern":"Invalid PMTiles file URL: (.+?)\\. URL must end with \\.pmtiles","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"admin/app/services/map_service.ts","lineNumber":261,"sourceCode":"            )\n          } catch (err) {\n            logger.warn(`[MapService] Failed to remove superseded file ${decision.path}:`, err)\n          }\n        } else if (decision.reason !== 'first_install' && decision.reason !== 'same_file') {\n          logger.info(\n            `[MapService] Kept prior ${parsed.resource_id} file (reason: ${decision.reason})`\n          )\n        }\n      } catch (error) {\n        logger.error(`[MapService] Failed to create InstalledResource for ${filename}:`, error)\n      }\n    }\n  }\n\n  async downloadRemote(url: string): Promise<{ filename: string; jobId?: string }> {\n    const parsed = new URL(url)\n    if (!parsed.pathname.endsWith('.pmtiles')) {\n      throw new Error(`Invalid PMTiles file URL: ${url}. URL must end with .pmtiles`)\n    }\n\n    const existing = await RunDownloadJob.getActiveByUrl(url)\n    if (existing) {\n      throw new Error(`Download already in progress for URL ${url}`)\n    }\n\n    const filename = url.split('/').pop()\n    if (!filename) {\n      throw new Error('Could not determine filename from URL')\n    }\n\n    const filepath = join(process.cwd(), this.mapStoragePath, 'pmtiles', filename)\n\n\n    // First, ensure base assets are present - regions depend on them\n    const baseAssetsExist = await this.ensureBaseAssets()\n    if (!baseAssetsExist) {","sourceCodeStart":243,"sourceCodeEnd":279,"githubUrl":"https://github.com/Crosstalk-Solutions/project-nomad/blob/0bd1c6f4f9888d577fe232de06ac144bb8337131/admin/app/services/map_service.ts#L243-L279","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nawait mapService.downloadRemote('https://tiles.example.com/style.json')\n\n// after\nawait mapService.downloadRemote('https://tiles.example.com/osm.pmtiles')","handlingStrategy":"validation","validationCode":"let u: URL\ntry { u = new URL(url) } catch { throw new BadRequestException('Invalid URL') }\nif (!u.pathname.toLowerCase().endsWith('.pmtiles'))\n  throw new BadRequestException('URL must point to a .pmtiles file')\nawait mapService.downloadRemote(url)","typeGuard":"const isPmtilesUrl = (url: string): boolean => {\n  try { return new URL(url).pathname.toLowerCase().endsWith('.pmtiles') }\n  catch { return false }\n}","tryCatchPattern":"try {\n  await mapService.downloadRemote(url)\n} catch (e) {\n  if ((e as Error).message.includes('must end with .pmtiles'))\n    return res.status(400).json({ error: 'Provide a direct .pmtiles archive URL' })\n  throw e\n}","preventionTips":["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"],"tags":["validation","url","pmtiles","maps"],"backgroundTag":"invalid-url-format","analyzedSha":"0bd1c6f4f9888d577fe232de06ac144bb8337131","analyzedAt":"2026-08-27T05:34:15.424Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}