S3 Checksums (AWS SDK v3 + Garage)
If your uploads fail with 400 InvalidDigest, your S3 client is sending the AWS SDK’s new default checksums, which Ghayma’s storage (Garage) rejects. Add requestChecksumCalculation: "WHEN_REQUIRED" and responseChecksumValidation: "WHEN_REQUIRED" to your S3Client config.
Symptom
Uploads — especially presigned PUT uploads from a browser — fail with:
400 InvalidDigest
Failed to validate checksum for algorithm Crc32: calculated ..., expected Crc32([0, 0, 0, 0])A plain PutObject from your server may fail the same way.
Cause
Starting with @aws-sdk/client-s3 v3.729.0 (January 2025), the SDK enables an
extra integrity checksum on every upload by default
(requestChecksumCalculation: "WHEN_SUPPORTED"). It adds an
x-amz-checksum-crc32 value to the request.
For presigned URLs the checksum is computed at the time the URL is signed —
when there is no body yet — so it is the CRC32 of an empty payload
([0, 0, 0, 0]). The browser then uploads the real file against that URL, and
Garage validates the embedded checksum against the actual bytes. They don’t
match, so the request is rejected with InvalidDigest.
AWS S3 itself ignores these parameters in this position, which is why the defaults work against AWS but break against S3-compatible stores. This is a known incompatibility that also affected Cloudflare R2, MinIO, Backblaze B2 and DigitalOcean Spaces when the default changed; the universal client-side fix is to only send a checksum when the operation actually requires one.
Fix
Set both checksum options to WHEN_REQUIRED when you construct the client.
This applies to every operation, including presigned URLs:
import { S3Client } from "@aws-sdk/client-s3";
const s3 = new S3Client({
endpoint: process.env.STORAGE_ENDPOINT,
region: process.env.STORAGE_REGION,
credentials: {
accessKeyId: process.env.STORAGE_ACCESS_KEY!,
secretAccessKey: process.env.STORAGE_SECRET_KEY!,
},
forcePathStyle: true, // required for Garage
// Garage rejects the SDK's default CRC32 checksums (400 InvalidDigest),
// especially on presigned PUTs — only send a checksum when required.
requestChecksumCalculation: "WHEN_REQUIRED",
responseChecksumValidation: "WHEN_REQUIRED",
});That’s the only change. Presigned-URL generation needs no extra flags — the options are read from the client the presigner is built from:
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
// `s3` is the WHEN_REQUIRED client above — the presigned URL will NOT
// carry the x-amz-checksum-* query parameters that break Garage.
const uploadUrl = await getSignedUrl(
s3,
new PutObjectCommand({ Bucket, Key, ContentType }),
{ expiresIn: 600 },
);This config is production-verified: presigned PUT uploads round-trip
byte-for-byte once both options are set to WHEN_REQUIRED.
Other languages
The same default change shipped across AWS SDKs in early 2025. The fix is the same idea everywhere.
Environment variables (any AWS SDK / CLI):
AWS_REQUEST_CHECKSUM_CALCULATION=when_required
AWS_RESPONSE_CHECKSUM_VALIDATION=when_requiredPython (boto3):
from boto3.session import Config
import boto3
s3 = boto3.client(
"s3",
endpoint_url=os.environ["STORAGE_ENDPOINT"],
config=Config(
s3={"addressing_style": "path"},
request_checksum_calculation="when_required",
response_checksum_validation="when_required",
),
)Go (aws-sdk-go-v2):
cfg.RequestChecksumCalculation = aws.RequestChecksumCalculationWhenRequired
cfg.ResponseChecksumValidation = aws.ResponseChecksumValidationWhenRequiredTroubleshooting
- Still seeing
InvalidDigest? Confirm the options are on the sameS3Clientinstance you pass togetSignedUrl— a presigned URL inherits the checksum behaviour of the client it was signed with. - Pinning instead of configuring also works as a stopgap: SDK versions
before
3.729.0don’t send the default checksum. ConfiguringWHEN_REQUIREDis preferred so you can stay current. - Multipart uploads from the browser have additional checksum caveats in the AWS SDK; for large files, prefer presigned single-part PUT or upload server-side.