hcengineering/platform · warning

Not found

Error message

Not found

What it means

When the requested file ends with '.hash', the server looks up statInfo of the file without the .hash suffix and returns its etag as text/plain; if statInfo returns null it falls through to 401 with body 'Not found'. Note the misleading 401 status — the file simply does not exist in the workspace's backup storage.

Source

Thrown at services/backup/backup-api-pod/src/server.ts:314

          'content-type': 'application/json',
          etag: jsonData.info?.lastTxId ?? ''
        })
        .end(JSON.stringify(jsonData))
      return
    }

    if (file.endsWith('.hash')) {
      // Just serve the file
      const fileInfo = await storage.statInfo(file.slice(0, file.length - 5))
      if (fileInfo != null) {
        res
          .status(200)
          .set({
            headers: { 'content-type': 'text/plain' }
          })
          .end(fileInfo.etag)
      }
      res.status(401).end('Not found')
      return
    }

    // Just serve the file
    const fileInfo = await storage.statInfo(file)
    if (fileInfo != null) {
      const responseHeaders = {
        'content-type': fileInfo.contentType ?? 'application/octet-stream',
        'content-length': fileInfo.size.toString(),
        etag: fileInfo.etag,
        'last-modified': new Date(fileInfo.lastModified).toUTCString()
      }

      // Check If-None-Match header
      const ifNoneMatch = headers['If-None-Match']
      if (ifNoneMatch === fileInfo.etag) {
        res.status(304).set(responseHeaders).end()
        return

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Check the file (without .hash) exists via /api/backup/<workspace>/index.json and use an exact listed name
  2. Re-run the backup if the underlying object was pruned
  3. Correct the filename spelling in your download script
  4. Treat this response as 404-style 'missing object' in client logic despite the 401 status

Example fix

// before
await fetch(`/api/backup/${ws}/${guess}.hash`)
// after
const idx = await (await fetch(`/api/backup/${ws}/index.json`)).json()
await Promise.all(idx.files.filter(f => f.name === target).map(f => fetch(`/api/backup/${ws}/${f.name}.hash`)))
Defensive patterns

Strategy: validation

Validate before calling

const index = await (await fetch(`/api/backup/${ws}/index.json`, { headers })).json()
const target = name + '.hash'
if (![...index.files.map((f: any) => f.name), ...index.files.map((f: any) => f.name + '.hash')].includes(target)) {
  throw new Error(`${target} not in backup index`)
}

Type guard

function fileInIndex(index: { files: { name: string }[] }, file: string): boolean {
  return index.files.some((f) => f.name === file || f.name + '.hash' === file)
}

Try / catch

const res = await fetch(hashUrl, { headers })
if (res.status === 401 && (await res.text()) === 'Not found') {
  console.warn('hash missing: underlying object absent from backup storage')
}

Prevention

When it happens

Trigger: GET /api/backup/<workspace>/<name>.hash where '<name>' (the .hash stripped) has no object in backup storage — e.g. requesting a hash for a file never backed up, an already-pruned snapshot file, or a misspelled filename.

Common situations: Scripts that fetch every listed file plus its .hash after a partial backup restore, stale index pages listing files removed from storage, or typos in generated file names.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29). Data as JSON: /api/errors/5c7dc31f22644947. Report an issue: GitHub.