{"record":{"id":"c360c4126491752d","repo":"neondatabase/neon","slug":"azure-get-response-contained-no-response-body","errorCode":null,"errorMessage":"Azure GET response contained no response body","messagePattern":"Azure GET response contained no response body","errorType":"exception","errorClass":"DownloadError","httpStatus":null,"severity":"error","filePath":"libs/remote_storage/src/azure_blob.rs","lineNumber":207,"sourceCode":"                .into_stream()\n                // convert to TryStream\n                .into_stream()\n                .map_err(to_download_error);\n\n            // apply per request timeout\n            let response = tokio_stream::StreamExt::timeout(response, timeout);\n\n            // flatten\n            let response = response.map(|res| match res {\n                Ok(res) => res,\n                Err(_elapsed) => Err(DownloadError::Timeout),\n            });\n\n            let mut response = Box::pin(response);\n\n            let Some(part) = response.next().await else {\n                return Err(DownloadError::Other(anyhow::anyhow!(\n                    \"Azure GET response contained no response body\"\n                )));\n            };\n            let part = part?;\n            if etag.is_none() {\n                etag = Some(part.blob.properties.etag);\n            }\n            if last_modified.is_none() {\n                last_modified = Some(part.blob.properties.last_modified.into());\n            }\n            if let Some(blob_meta) = part.blob.metadata {\n                metadata.extend(blob_meta.iter().map(|(k, v)| (k.to_owned(), v.to_owned())));\n            }\n\n            // unwrap safety: if these were None, bufs would be empty and we would have returned an error already\n            let etag = etag.unwrap();\n            let last_modified = last_modified.unwrap();\n\n            let tail_stream = response","sourceCodeStart":189,"sourceCodeEnd":225,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/libs/remote_storage/src/azure_blob.rs#L189-L225","documentation":"In download_for_builder, the Azure blob GET is turned into a stream; the first next() must yield at least one response part carrying the blob's etag/last_modified/metadata. If the stream ends before yielding anything (zero parts), the code cannot even construct the Download and returns DownloadError::Other with this message. An actual network/HTTP failure surfaces as a different DownloadError variant, so this specifically means: request nominally succeeded but the body stream was empty.","triggerScenarios":"Azure returning an empty response stream for a GET blob call -- transient service weirdness, a race where the timeout wrapper fires before the first part is delivered but is reported as stream end, or SDK-level desync after connection reuse. Rare, and typically intermittent rather than deterministic.","commonSituations":"Flaky egress paths or middleboxes truncating chunked responses; retries hitting an Azure node mid-failover; a zero-byte blob accessed through a code path that still expects header metadata parts; stress tests saturating the connection pool so streams complete prematurely.","solutions":["Retry the download: this error is almost always transient (wrap with backoff, e.g. 3 attempts)","Check Azure status / storage account metrics if it repeats for the same key","Verify the blob exists and is non-empty via az CLI or another tool to rule out a real data issue","Upgrade the azure_storage_blobs SDK if an older version mishandles connection reuse"],"exampleFix":"// before\nlet download = remote_storage.download(&path).await?;\n\n// after: retry the transient empty-body case with backoff\nlet download = backoff::future::retry_notify(\n    backoff::ExponentialBackoff::default(),\n    || async {\n        match remote_storage.download(&path).await {\n            Err(DownloadError::Other(e))\n                if e.to_string().contains(\"no response body\") =>\n            {\n                Err(backoff::Error::transient(e))\n            }\n            other => other.map_err(backoff::Error::permanent),\n        }\n    },\n    |e, d| tracing::warn!(\"azure empty body retry after {d:?}: {e:#}\"),\n)\n.await?;","handlingStrategy":"retry","validationCode":null,"typeGuard":"fn is_empty_body_download_error(e: &DownloadError) -> bool {\n    matches!(e, DownloadError::Other(inner)\n        if inner.to_string().contains(\"no response body\"))\n}","tryCatchPattern":"use backoff::ExponentialBackoff;\n\n// 'no response body' is transient Azure weirdness: retry, don't propagate.\nlet download = backoff::future::retry(\n    ExponentialBackoff::default(),\n    || async {\n        match storage.download(&path, &cancel).await {\n            Err(DownloadError::Other(e))\n                if e.to_string().contains(\"no response body\") =>\n            {\n                tracing::warn!(?path, \"azure GET returned empty body; retrying\");\n                Err(backoff::Error::transient(e))\n            }\n            Err(DownloadError::Timeout) => {\n                // timeouts are also transient for this endpoint\n                Err(backoff::Error::transient(anyhow::anyhow!(\"download timeout\")))\n            }\n            other => other.map_err(backoff::Error::permanent),\n        }\n    },\n)\n.await?;","preventionTips":["Wrap all remote-storage downloads in retry-with-backoff; classify empty-body and Timeout as transient, 404 as permanent","Track retry rate per storage backend -- a rise in empty-body errors predicts egress or Azure incidents","Keep the azure SDK and connection pool settings current; stale pooled connections are a known source of truncated streams","Never treat one empty-body failure as data corruption; verify blob existence with a second path before escalating"],"tags":["rust","azure","remote-storage","download","transient"],"backgroundTag":"cloud-storage-download-failed","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}