neondatabase/neon · error · DownloadError

Azure GET response contained no response body

Error message

Azure GET response contained no response body

What it means

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.

Source

Thrown at libs/remote_storage/src/azure_blob.rs:207

                .into_stream()
                // convert to TryStream
                .into_stream()
                .map_err(to_download_error);

            // apply per request timeout
            let response = tokio_stream::StreamExt::timeout(response, timeout);

            // flatten
            let response = response.map(|res| match res {
                Ok(res) => res,
                Err(_elapsed) => Err(DownloadError::Timeout),
            });

            let mut response = Box::pin(response);

            let Some(part) = response.next().await else {
                return Err(DownloadError::Other(anyhow::anyhow!(
                    "Azure GET response contained no response body"
                )));
            };
            let part = part?;
            if etag.is_none() {
                etag = Some(part.blob.properties.etag);
            }
            if last_modified.is_none() {
                last_modified = Some(part.blob.properties.last_modified.into());
            }
            if let Some(blob_meta) = part.blob.metadata {
                metadata.extend(blob_meta.iter().map(|(k, v)| (k.to_owned(), v.to_owned())));
            }

            // unwrap safety: if these were None, bufs would be empty and we would have returned an error already
            let etag = etag.unwrap();
            let last_modified = last_modified.unwrap();

            let tail_stream = response

View on GitHub (pinned to 8f60b04da4)

Solutions

  1. Retry the download: this error is almost always transient (wrap with backoff, e.g. 3 attempts)
  2. Check Azure status / storage account metrics if it repeats for the same key
  3. Verify the blob exists and is non-empty via az CLI or another tool to rule out a real data issue
  4. Upgrade the azure_storage_blobs SDK if an older version mishandles connection reuse

Example fix

// before
let download = remote_storage.download(&path).await?;

// after: retry the transient empty-body case with backoff
let download = backoff::future::retry_notify(
    backoff::ExponentialBackoff::default(),
    || async {
        match remote_storage.download(&path).await {
            Err(DownloadError::Other(e))
                if e.to_string().contains("no response body") =>
            {
                Err(backoff::Error::transient(e))
            }
            other => other.map_err(backoff::Error::permanent),
        }
    },
    |e, d| tracing::warn!("azure empty body retry after {d:?}: {e:#}"),
)
.await?;
Defensive patterns

Strategy: retry

Type guard

fn is_empty_body_download_error(e: &DownloadError) -> bool {
    matches!(e, DownloadError::Other(inner)
        if inner.to_string().contains("no response body"))
}

Try / catch

use backoff::ExponentialBackoff;

// 'no response body' is transient Azure weirdness: retry, don't propagate.
let download = backoff::future::retry(
    ExponentialBackoff::default(),
    || async {
        match storage.download(&path, &cancel).await {
            Err(DownloadError::Other(e))
                if e.to_string().contains("no response body") =>
            {
                tracing::warn!(?path, "azure GET returned empty body; retrying");
                Err(backoff::Error::transient(e))
            }
            Err(DownloadError::Timeout) => {
                // timeouts are also transient for this endpoint
                Err(backoff::Error::transient(anyhow::anyhow!("download timeout")))
            }
            other => other.map_err(backoff::Error::permanent),
        }
    },
)
.await?;

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16). Data as JSON: /api/errors/c360c4126491752d. Report an issue: GitHub.