{"record":{"id":"4de3436be74dbd3f","repo":"toeverything/AFFiNE","slug":"blob-invalid-4de343","errorCode":"blob_invalid","errorMessage":"Missing upload content length","messagePattern":"Missing upload content length","errorType":"exception","errorClass":"BlobInvalid","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/core/storage/wrappers/blob.ts","lineNumber":376,"sourceCode":"    if (!usePresignedURL?.enabled) {\n      return;\n    }\n    return {\n      signKey: usePresignedURL.signKey || undefined,\n      urlPrefix: usePresignedURL.urlPrefix || undefined,\n    };\n  }\n\n  private createProxyUploadUrl(\n    workspaceId: string,\n    key: string,\n    metadata: PutObjectMetadata | undefined,\n    proxy: UploadProxyConfig\n  ) {\n    const contentType = metadata?.contentType ?? 'application/octet-stream';\n    const contentLength = metadata?.contentLength;\n    if (contentLength === undefined) {\n      throw new BlobInvalid('Missing upload content length');\n    }\n    const expiresAt = new Date(Date.now() + SIGNED_URL_EXPIRED * 1000);\n    const expiresAtSeconds = Math.floor(expiresAt.getTime() / 1000);\n    const token = createStorageUploadToken(\n      PROXY_UPLOAD_PATH,\n      [workspaceId, key, contentType, contentLength],\n      expiresAtSeconds,\n      proxy.signKey\n    );\n    return {\n      url: this.linkProxyUrl(proxy.urlPrefix, PROXY_UPLOAD_PATH, {\n        workspaceId,\n        key,\n        contentType,\n        contentLength,\n        expiresAt: expiresAtSeconds,\n        token,\n      }),","sourceCodeStart":358,"sourceCodeEnd":394,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/storage/wrappers/blob.ts#L358-L394","documentation":"BlobInvalid('Missing upload content length') at blob.ts:376, inside `WorkspaceBlobStorage.createProxyUploadUrl`. When the proxy (signed-URL) mode is enabled, building a proxy upload URL requires `metadata.contentLength`; if undefined, the URL cannot be token-signed (contentLength is a canonical token field) so the call is rejected before minting.","triggerScenarios":"Calling `presignPut(workspaceId, key, metadata)` while `usePresignedURL.enabled` + `signKey` are set in storage config, and `metadata` is undefined or has no `contentLength`. The proxy branch is taken (`config.signKey` truthy) and `metadata?.contentLength` is undefined.","commonSituations":"Internal caller passes `{ contentType }` only, assuming the server can infer size; caller passes `metadata: undefined`; refactor dropped the contentLength field; proxy mode newly enabled without updating callers.","solutions":["Always pass `metadata.contentLength` (the exact byte size of the blob) when calling `presignPut` under proxy mode.","If the size is unknown, switch to non-proxy presign (unset `usePresignedURL.enabled`/`signKey`) or buffer the blob first to measure it.","Type the call site so contentLength is required when proxy mode is on."],"exampleFix":"// before\nconst url = await blob.presignPut(ws, key, { contentType }); // proxy mode -> 273\n\n// after\nconst url = await blob.presignPut(ws, key, { contentType, contentLength: buf.byteLength });","handlingStrategy":"validation","validationCode":"// Always supply contentLength when proxy mode is enabled.\nfunction assertProxyMetadata(metadata: { contentType?: string; contentLength?: number } | undefined) {\n  if (metadata?.contentLength === undefined) {\n    throw new Error('metadata.contentLength is required in proxy upload mode');\n  }\n}\nassertProxyMetadata(metadata);\nconst url = await blob.presignPut(ws, key, { contentType, contentLength: buf.byteLength });","typeGuard":"function hasContentLength(m: unknown): m is { contentType?: string; contentLength: number } {\n  return typeof m === 'object' && m !== null && typeof (m as any).contentLength === 'number';\n}","tryCatchPattern":null,"preventionTips":["Buffer the blob to measure byteLength before presigning under proxy mode.","Make contentLength required at the type level when `usePresignedURL.enabled && signKey`.","Audit presignPut call sites after enabling proxy mode."],"tags":["upload","validation","presign","content-length"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}