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
- Fix at the call site or upstream: treat missing items as an empty page — res.items.unwrap_or_default() instead of ok_or_else(...)
- Verify the bucket name and prefix actually contain objects before calling
- If data is expected, confirm objects were uploaded with the exact key prefix being listed
- 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
- Never assume an error from a listing means the service is broken — empty buckets are legal states
- Verify bucket names and prefixes in configuration before first use
- If you control the library, patch items to unwrap_or_default() and contribute the fix upstream
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
- max keys reached
- no 'updated' field
- Missing size (content length) header
- GCS PUT error \n\t {:?}
- Boundary string from 'multipart/related' HTTP upload occurre
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/0dfcc11f8793f11f.
Report an issue: GitHub.