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

  1. Set the blob storage provider to cloudflare-r2 or aws-s3 in the server configuration.
  2. 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

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)