neondatabase/neon · error · DownloadError

no items returned

Error message

no items returned

What it means

While paginating the GCS JSON API list-objects-with-versions response, the parsed GCSListResponse had no items field and this code treats that as an error. The GCS JSON API legitimately omits items when a page contains zero matching objects (empty bucket/prefix, or a trailing empty page), so this usually fires on a valid empty listing rather than a real protocol failure.

Source

Thrown at libs/remote_storage/src/gcs_bucket.rs:277

                    warn_threshold,
                    max_retries,
                    "listing object versions",
                    cancel,
                ) 
                .await
                .ok_or_else(|| DownloadError::Cancelled)
                .and_then(|x| x)?;
                
            let res = response.json::<GCSListResponse>()
                .await
                .map_err(|e| DownloadError::Other(e.into()))?;
                    
            // fill up our results vec, 
            continuation_token = res.next_page_token;
            
            let version_listing = 
                res.items
                    .ok_or_else(|| DownloadError::Other(anyhow::anyhow!("no items returned")))?
                    .into_iter()
                    .map(| GCSObject { name, updated, time_deleted, generation, .. } | {
                        // don't `filter_map`, a `None` for `last_modified` ('updated') is bad for
                        // time travel, so catch it.
                        if updated.is_none() {
                           return Err(
                               DownloadError::Other(
                                   anyhow::anyhow!("no 'updated' field")
                               )
                           )
                        }
                        Ok(
                            GCSVersion {
                                key: self.gcs_object_to_relative_path(&name),
                                last_modified: to_system_time(updated).unwrap(),
                                id: VersionId(generation.expect("no version id")),
                                time_deleted: to_system_time(time_deleted),
                            }

View on GitHub (pinned to 8f60b04da4)

Solutions

  1. Fix at the call site or upstream: treat missing items as an empty page — res.items.unwrap_or_default() instead of ok_or_else(...)
  2. Verify the bucket name and prefix actually contain objects before calling
  3. If data is expected, confirm objects were uploaded with the exact key prefix being listed
  4. Add a preflight check (a single-page list without continuation) to distinguish 'empty' from 'error'

Example fix

// before: empty page -> hard error
let version_listing = res.items
    .ok_or_else(|| DownloadError::Other(anyhow::anyhow!("no items returned")))?
    .into_iter()
    .map(/* ... */);

// after: empty page -> empty page of results, loop exits via absent continuation token
let version_listing = res.items
    .unwrap_or_default()
    .into_iter()
    .map(/* ... */);
Defensive patterns

Strategy: fallback

Validate before calling

// Confirm the prefix actually has objects before relying on the listing result.
async fn prefix_populated(storage: &GenericRemoteStorage, prefix: &RemotePath, cancel: &CancellationToken) -> bool {
    matches!(storage.list_files(Some(prefix), ListingMode::NoDelimiter, cancel).await, Ok(files) if !files.is_empty())
}

Try / catch

// GCS omits `items` on empty pages: treat this specific error as an empty listing.
let versions = match storage.list_versions(key, None, &cancel).await {
    Ok(v) => v,
    Err(DownloadError::Other(e)) if format!("{e:#}").contains("no items returned") => {
        tracing::debug!("empty GCS listing for {key}");
        Default::default()
    }
    Err(e) => return Err(e),
};

Prevention

When it happens

Trigger: Calling list_versions/list with a prefix that matches zero objects; listing an empty bucket; a final pagination round-trip whose continuation token yields no items; wrong bucket name so nothing matches.

Common situations: Tenants/prefixes with no uploaded objects yet; typo'd prefix; time travel against a freshly created bucket. Any empty result set turns into DownloadError::Other("no items returned") instead of an empty list.

Related errors


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