{"record":{"id":"39c3b62f13755bb6","repo":"hcengineering/platform","slug":"empty-chunk-received-emptychunkretries-times-fo","errorCode":null,"errorMessage":"Empty chunk received ${emptyChunkRetries} times for blob ${name} at offset ${written}/${size}","messagePattern":"Empty chunk received (.+?) times for blob (.+?) at offset (.+?)/(.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"foundations/server/packages/client/src/blob.ts","lineNumber":78,"sourceCode":"            readable.on('data', (chunk) => {\n              chunks.push(chunk)\n            })\n            readable.on('end', () => {\n              readable.destroy()\n              resolve()\n            })\n          })\n          const chunk = Buffer.concat(chunks)\n\n          // Check for empty chunk to prevent infinite loop\n          if (chunk.length === 0) {\n            emptyChunkRetries++\n            if (emptyChunkRetries >= maxEmptyChunkRetries) {\n              ctx.error('Empty chunk received multiple times, aborting', { name, written, size, emptyChunkRetries })\n              await new Promise<void>((resolve) => {\n                writable.end(resolve)\n              })\n              throw new Error(\n                `Empty chunk received ${emptyChunkRetries} times for blob ${name} at offset ${written}/${size}`\n              )\n            }\n            ctx.warn('Empty chunk received, retrying', { name, written, size, retry: emptyChunkRetries })\n            await new Promise<void>((resolve) => setTimeout(resolve, 100 * emptyChunkRetries))\n            continue\n          }\n          emptyChunkRetries = 0 // Reset on successful non-empty chunk\n\n          await new Promise<void>((resolve, reject) => {\n            writable.write(chunk, (err) => {\n              if (err != null) {\n                reject(err)\n              }\n              resolve()\n            })\n          })\n","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/foundations/server/packages/client/src/blob.ts#L60-L96","documentation":"In the blob upload writer (writeTo), reading the source stream may repeatedly yield empty chunks. After maxEmptyChunkRetries empty reads, the writer ends the writable and throws this error including the blob name, bytes written vs. total size, and the retry count. It protects against infinite loops when a stream/socket keeps returning zero bytes without an error or EOF.","triggerScenarios":"Uploading/downloading a blob whose underlying stream stalls and returns empty chunks (e.g. a broken or stalled HTTP/socket source) more than maxEmptyChunkRetries consecutive times; the source reports no data and no end-of-stream, so the retry loop exhausts.","commonSituations":"Flaky network or proxy cutting the connection mid-transfer while the reader doesn't emit an error; server-side stream hang; uploading large blobs over unstable connections in dev tools or domain processing.","solutions":["Check network stability/proxy timeouts between client and blob storage; retry the transfer","Verify the source that produces the stream (URL, file, socket) actually serves the full blob and supports range reads at the failed offset","Increase maxEmptyChunkRetries / adjust the backoff if transfers over slow links legitimately pause","Resume the upload from the reported offset (written) rather than restarting from zero"],"exampleFix":"// before (transfer aborts after repeated empty chunks)\nawait client.blob.put(name, unstableStream)\n// after (wrap with a retrying source and resume from offset)\nawait withRetry((attempt) => client.blob.put(name, makeFreshStream(attempt)), (err) => !String(err).includes('Empty chunk'), 2000)","handlingStrategy":"retry","validationCode":"// sanity-check the source serves data before a long upload\nconst probe = await fetch(sourceUrl, { headers: { Range: 'bytes=0-0' } })\nif (!probe.ok) throw new Error(`Source unavailable: ${probe.status}`)","typeGuard":null,"tryCatchPattern":"try {\n  await blobWriteTo(writable, source)\n} catch (err) {\n  if (/Empty chunk received/.test(err.message)) {\n    const [, written, size] = err.message.match(/offset (\\d+)\\/(\\d+)/) || []\n    // resume from Number(written) with a fresh stream/connection\n  } else throw err\n}","preventionTips":["Use stable networks/long-enough proxy timeouts for large blob transfers","Verify the source stream/URL supports the required reads (ranges, keep-alive)","Tune maxEmptyChunkRetries for slow links","Resume from the failed offset instead of restarting the whole transfer"],"tags":["blob","network","streaming","retry-exhausted"],"backgroundTag":"empty-chunk-stream","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}