hcengineering/platform · error · ReadonlyError
ReadonlyError
Error message
ReadonlyError
What it means
ReadonlyStorageAdapter wraps a real StorageAdapter and deliberately throws ReadonlyError('Readonly mode') from mutating methods. make() (which creates/initializes a workspace bucket/prefix) is one of them. The wrapper is enabled when a storage config has readonly=true (see createStorageFromConfig). It is an intentional policy rejection, not a bug — all writes are blocked while reads delegate to the underlying adapter.
Source
Thrown at foundations/server/packages/server-storage/src/readonly.ts:43
}
export class ReadonlyStorageAdapter implements StorageAdapter {
constructor (private readonly adapter: StorageAdapter) {}
async initialize (ctx: MeasureContext, wsIds: WorkspaceIds): Promise<void> {
await this.adapter.initialize(ctx, wsIds)
}
async close (): Promise<void> {
await this.adapter.close()
}
async exists (ctx: MeasureContext, wsIds: WorkspaceIds): Promise<boolean> {
return await this.adapter.exists(ctx, wsIds)
}
async make (ctx: MeasureContext, wsIds: WorkspaceIds): Promise<void> {
throw new ReadonlyError()
}
async listBuckets (ctx: MeasureContext): Promise<BucketInfo[]> {
return await this.adapter.listBuckets(ctx)
}
async delete (ctx: MeasureContext, wsIds: WorkspaceIds): Promise<void> {
throw new ReadonlyError()
}
async remove (ctx: MeasureContext, wsIds: WorkspaceIds, objectNames: string[]): Promise<void> {
throw new ReadonlyError()
}
async listStream (ctx: MeasureContext, wsIds: WorkspaceIds): Promise<BlobStorageIterator> {
return await this.adapter.listStream(ctx, wsIds)
}
View on GitHub (pinned to 63e28dc964)
Solutions
- Remove the readonly flag from the storage URI in STORAGE_CONFIG (e.g. drop &readonly=true) for the storage that must accept writes
- If the bucket genuinely must stay read-only, create the workspace in a different writable storage and mount the readonly one only as a fallback/read source
- Verify which adapter throws by logging config.kind/name before createStorageFromConfig
- Check that a recent deployment didn't flip readonly=true unintentionally
Example fix
// before STORAGE_CONFIG=minio|minio:9000?accessKey=minio&secretKey=minio&readonly=true // after STORAGE_CONFIG=minio|minio:9000?accessKey=minio&secretKey=minio
Defensive patterns
Strategy: validation
Validate before calling
// before calling make(), ensure the target storage is writable
const cfg = storageConfig.storages.find(s => s.name === targetName)
if (cfg?.readonly === 'true') {
throw new Error(`storage ${targetName} is readonly; make() would fail`)
} Type guard
function isReadonlyStorageAdapter (a: StorageAdapter): a is ReadonlyStorageAdapter {
return a instanceof ReadonlyStorageAdapter
} Try / catch
try {
await storage.make(ctx, wsIds)
} catch (err) {
if (err.name === 'ReadonlyError') {
// storage is intentionally immutable; create workspace elsewhere or skip
return skipWithWarning(wsIds)
}
throw err
} Prevention
- Keep readonly=true only on fallback/read replicas in STORAGE_CONFIG
- Check the readonly flag at service startup and fail fast if the primary is readonly
- Document which environments run against read-only storage
- When copying STORAGE_CONFIG between envs, re-check the readonly params
When it happens
Trigger: Any code path calling make(ctx, wsIds) on a ReadonlyStorageAdapter — typically server startup or first access to a workspace that doesn't exist yet in a storage configured with readonly=true in STORAGE_CONFIG (e.g. appending '|readonly=true' or setting the readonly query param on the URI), or programmatic use of ReadonlyStorageAdapter as a wrapper.
Common situations: Pointing a full (writable) service at a read-only replica/backup bucket by mistake; migrating between storages where the new primary is mistakenly marked readonly; copy-pasting a STORAGE_CONFIG example that included readonly=true; disaster-recovery runbook executed with the read-only flag still set.
Related errors
- Low level storage is not available
- No AdapterManager
- Storage adapter should be specified
- Low level storage should be specified
- Invalid storage config:${st}
AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29).
Data as JSON: /api/errors/4d98e91e9a2c2da1.
Report an issue: GitHub.