Freika/dawarich · error · Error

Failed to fetch points: ${response.statusText}

Error message

Failed to fetch points: ${response.statusText}

What it means

ApiClient.fetchPoints throws when GET /api/v1/points?start_at&end_at&page&per_page&slim=true&order=asc(&import_id) returns a non-OK status. The message embeds response.statusText, which is frequently an empty string over HTTP/2 and h2c proxies, so the real cause (401 bad Bearer key, 422 invalid date range, 500 server error) is invisible in the message. Pagination headers X-Current-Page / X-Total-Pages are only read on success.

Source

Thrown at app/javascript/maps_maplibre/services/api_client.js:35

  async fetchPoints({ start_at, end_at, page = 1, per_page = 1000, signal }) {
    const params = new URLSearchParams({
      start_at,
      end_at,
      page: page.toString(),
      per_page: per_page.toString(),
      slim: "true",
      order: "asc",
    })

    if (this.importId) params.append("import_id", this.importId)

    const response = await fetch(`${this.baseURL}/points?${params}`, {
      headers: this.getHeaders(),
      signal,
    })

    if (!response.ok) {
      throw new Error(`Failed to fetch points: ${response.statusText}`)
    }

    const points = await response.json()

    return {
      points,
      currentPage: parseInt(response.headers.get("X-Current-Page") || "1", 10),
      totalPages: parseInt(response.headers.get("X-Total-Pages") || "1", 10),
      totalPointsInRange: parseInt(
        response.headers.get("X-Total-Points-In-Range") || "0",
        10,
      ),
      scopedPoints: parseInt(
        response.headers.get("X-Scoped-Points") || "0",
        10,
      ),
    }
  }

View on GitHub (pinned to 97fad417c5)

Solutions

  1. Log response.status alongside statusText (HTTP/2 makes statusText empty) to identify 401 vs 422 vs 500
  2. Verify the api key passed to new ApiClient(apiKey, importId) is the user's current key
  3. Confirm start_at/end_at are sent and ISO-formatted before the fetch
  4. If importId is set, check the import still exists and belongs to the current user
  5. Wrap fetchPoints callers in try/catch and keep the map in a recoverable 'load failed' state instead of a blank canvas

Example fix

// before
if (!response.ok) {
  throw new Error(`Failed to fetch points: ${response.statusText}`)
}

// after
if (!response.ok) {
  throw new Error(
    `Failed to fetch points: ${response.status} ${response.statusText}`.trim(),
  )
}
Defensive patterns

Strategy: try-catch

Validate before calling

function assertFetchPointsArgs({ start_at, end_at, apiKey }) {
  if (!apiKey) throw new Error('API key is required')
  const s = Date.parse(start_at)
  const e = Date.parse(end_at)
  if (Number.isNaN(s) || Number.isNaN(e)) throw new Error('start_at/end_at must be valid dates')
  if (s > e) throw new Error('start_at must be before end_at')
}

Try / catch

try {
  const result = await client.fetchPoints({ start_at, end_at, page, signal })
} catch (error) {
  console.error(`fetchPoints failed on page ${page}:`, error.message)
  renderMapErrorState() // keep the map usable instead of a blank canvas
}

Prevention

When it happens

Trigger: Maps V2 initial load or date-range change with an invalid/expired apiKey passed to the ApiClient constructor (401), missing or non-ISO start_at/end_at (422), an import_id scoping to a deleted import, or a 500 from the points endpoint. Also thrown when the user's session ends mid-pagination while fetching subsequent pages.

Common situations: Rotated or blank API key in the map element's data attribute, date pickers emitting local-time strings the backend cannot parse, stale import_id kept in page state after re-importing data, server-side timeout on huge date ranges, HTTP/2 deployment stripping statusText so the toast shows just 'Failed to fetch points: '.

Related errors


AI-assisted analysis of Freika/dawarich@97fad417c5 (2026-08-21). Data as JSON: /api/errors/f112854d76f14b3f. Report an issue: GitHub.