GCS S3-Compatible API ACL Rejection

SkillFiles & storage

Fix S3 "InvalidArgument" errors when using Google Cloud Storage S3-compatible API with ObjectCannedAcl::PublicRead or x-amz-acl headers. Use when: (1) put_object or upload_blob fails with InvalidArgument on GCS, (2) Using aws-sdk-s3 Rust crate with GCS endpoint (storage.googleapis.com), (3) Bucket has uniform bucket-level access enabled (GCS default since 2023), (4) S3 operations work on MinIO/AWS but fail on GCS. GCS rejects per-object ACLs when uniform bucket-level access is enabled.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the GCS S3-Compatible API ACL Rejection skill

What this skill tells your AI

The instructions your AI receives, as published by divinevideo/divine-mobile in .agents/skills/gcs-s3-compat-acl-rejection/SKILL.md and read by ahel’s review.

Problem

S3 put_object calls fail with InvalidArgument when targeting Google Cloud Storage via its S3-compatible API, despite working correctly with MinIO or AWS S3.

Context / Trigger Conditions

  • Using aws-sdk-s3 (Rust) or any S3 SDK with GCS endpoint
  • AWS_ENDPOINT=https://storage.googleapis.com
  • Error: Error { code: "InvalidArgument", message: "Invalid argument." }
  • Stack trace shows S3BlobStore::put_temp or similar S3 put operation
  • The GCS bucket has uniform bucket-level access enabled (GCS default)

Solution

Remove ObjectCannedAcl::PublicRead from put_object calls when using GCS:

// BEFORE (fails on GCS with uniform bucket-level access)
self.client
    .put_object()
    .body(body)
    .bucket(&self.s3_bucket)
    .key(key)
    .acl(ObjectCannedAcl::PublicRead)  // This causes InvalidArgument
    .send()
    .await?;

// AFTER (works with GCS)
self.client
    .put_object()
    .body(body)
    .bucket(&self.s3_bucket)
    .key(key)
    // Remove .acl() — use bucket-level IAM instead
    .send()
    .await?;

For public read access, configure the bucket IAM policy instead:

gsutil iam ch allUsers:objectViewer gs://YOUR_BUCKET

Or disable uniform bucket-level access (not recommended):

gsutil ubla set off gs://YOUR_BUCKET

Verification

After removing the .acl() call, upload should succeed:

curl -X POST "$PDS_URL/xrpc/com.atproto.repo.uploadBlob" \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: video/mp4" \
  --data-binary @test.mp4

Notes

  • GCS uniform bucket-level access has been the default since 2023
  • The copy_object call in move_object() also uses .acl(PublicRead) and will fail
  • This affects rsky-pds blob storage: rsky-pds/src/actor_store/aws/s3.rs lines 69, 97, 212
  • MinIO doesn't enforce this restriction, so local dev works but production GCS fails
  • The same issue applies to put_permanent() and move_object() methods

Signals

GitHub stars
265
Forks
55
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
gcs-s3-compat-acl-rejection
Source
github.com/divinevideo/divine-mobile