toeverything/AFFiNE · error · BlobInvalid
blob_invalid
blob_invalid
Error message
Invalid endpoint
What it means
BlobInvalid('Invalid endpoint') at packages/backend/server/src/core/storage/r2-proxy.ts:42. getUploadProxyConfig only proxies presigned uploads for S3-compatible providers 'cloudflare-r2' and 'aws-s3'; any other value of config.storages.blob.storage.provider aborts with this error.
Solutions
- Set the blob storage provider to cloudflare-r2 or aws-s3 in the server configuration.
- Do not direct clients to the upload proxy endpoint when running non-S3 storage.
Example fix
# before [blob] storage.provider = 'fs' # after [blob.storage] provider = 'cloudflare-r2'
Defensive patterns
Strategy: validation
Validate before calling
const provider = config.storages.blob.storage.provider;
const proxySupported = provider === 'cloudflare-r2' || provider === 'aws-s3';
if (!proxySupported) {
throw new Error(`upload proxy unavailable for provider ${provider}`);
} Type guard
const isBlobInvalid = (e: unknown): e is BlobInvalid => e instanceof BlobInvalid;
Try / catch
try {
await proxy.upload(...);
} catch (e) {
if (e instanceof BlobInvalid && e.message === 'Invalid endpoint') {
// storage backend is not S3-compatible; route via direct server upload instead
}
} Prevention
- Fail fast at startup: assert the blob provider is S3-compatible before mounting the proxy.
- Keep environment-specific configs aligned so dev and prod use supported providers.
When it happens
Trigger: PUT /upload on the R2 upload proxy while the blob storage provider is configured as filesystem or anything other than cloudflare-r2/aws-s3.
Common situations: Local dev defaults (non-S3 storage) pointed at the proxy, a provider name typo in config, or routing all uploads through the proxy regardless of backend.
Related errors
AI-assisted analysis of toeverything/AFFiNE@2af30773ae (2026-08-18).
Data as JSON: /api/errors/44b1773cb97ca1af.
Report an issue: GitHub.
Appendix: source
Thrown at packages/backend/server/src/core/storage/r2-proxy.ts:42
type UploadProxyConfig = {
signKey: string;
};
@Controller(STORAGE_PROXY_ROOT)
export class R2UploadController {
private readonly logger = new Logger(R2UploadController.name);
constructor(
private readonly config: Config,
private readonly models: Models,
private readonly rt: StorageRuntimeProvider
) {}
private getUploadProxyConfig(): UploadProxyConfig {
const storage = this.config.storages.blob.storage as StorageProviderConfig;
if (storage.provider !== 'cloudflare-r2' && storage.provider !== 'aws-s3') {
throw new BlobInvalid('Invalid endpoint');
}
const uploadConfig = (storage.config as S3StorageConfig).usePresignedURL;
const signKey = uploadConfig?.signKey;
if (!uploadConfig?.enabled || !signKey) {
throw new BlobInvalid('Invalid endpoint');
}
return { signKey };
}
private safeEqual(expected: string, actual: string) {
const a = Buffer.from(expected);
const b = Buffer.from(actual);
if (a.length !== b.length) {
return false;
}
return timingSafeEqual(a, b);View on GitHub (pinned to 2af30773ae)