hcengineering/platform · error · NotFoundError

text (response body)

Error message

text (response body)

What it means

wrappedFetch in the storage client converts failed HTTP responses into typed errors. When the server responds with a non-ok status, the response body text is captured and thrown: NotFoundError for 404, StorageError for any other failure. The error message is the raw response body, so the actual reason lives in the server's response text.

Source

Thrown at foundations/core/packages/api-client/src/storage/client.ts:192

      chunks.push(chunk)
    }
    return Buffer.concat(chunks as any)
  } else {
    throw new TypeError('Unsupported data type')
  }
}

async function wrappedFetch (url: string | URL, init?: RequestInit): Promise<Response> {
  let response: Response
  try {
    response = await fetch(url, init)
  } catch (error: any) {
    throw new NetworkError(`Network error ${error}`)
  }
  if (!response.ok) {
    const text = await response.text()
    if (response.status === 404) {
      throw new NotFoundError(text)
    } else {
      throw new StorageError(text)
    }
  }
  return response
}

export function createStorageClient (
  filesUrl: string,
  uploadUrl: string,
  token: string,
  workspace: WorkspaceUuid
): StorageClient {
  return new StorageClientImpl(filesUrl, uploadUrl, token, workspace)
}

export async function connectStorage (url: string, options: AuthOptions, config?: ServerConfig): Promise<StorageClient> {
  config ??= await loadServerConfig(url)

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Log the error message — it is the raw response body — to see the server's actual reason.
  2. For 404: verify the blob/document ID exists before calling stat/response/remove, or treat NotFoundError as an expected miss.
  3. Check the storage endpoint URL and credentials used to create the storage client.
  4. Retry with backoff for 5xx statuses, as these usually indicate transient storage-service problems.
  5. Capture response.status/text in the error for observability instead of discarding the status code.

Example fix

// before
try {
  await storage.stat(blobId)
} catch (err) {
  console.error('failed', err) // message is opaque body text
}
// after
import { NotFoundError } from '@hcengineering/api-client'
try {
  await storage.stat(blobId)
} catch (err) {
  if (err instanceof NotFoundError) return null // expected miss
  console.error('storage status/body:', err.message)
  throw err
}
Defensive patterns

Strategy: try-catch

Validate before calling

// optionally probe before use
const res = await fetch(`${storageUrl}/${blobId}`, { method: 'HEAD' })
if (res.status === 404) return null

Type guard

function isNotFound(err: unknown): err is NotFoundError {
  return err instanceof NotFoundError
}

Try / catch

try {
  return await storage.stat(blobId)
} catch (err) {
  if (err instanceof NotFoundError) return null
  if (err instanceof StorageError) console.error('storage:', err.message)
  throw err
}

Prevention

When it happens

Trigger: Any storage API call (stat, response, remove) that reaches wrappedFetch and receives response.ok === false: missing blobs (404 -> NotFoundError), permission denied, malformed requests, or storage-server outages (other statuses -> StorageError).

Common situations: Requesting a blob ID that was deleted or never uploaded; wrong storage endpoint URL in config; expired or missing auth token causing 401/403; storage service temporarily down returning 5xx.

Related errors


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