PutObjectLegalHold
PutObjectLegalHold places a legal hold on an object version (ON) or lifts it (OFF). While a
version is held, it cannot be deleted or overwritten, and it stays that way until the hold is
lifted. A hold has no end date and is independent of any retention period, which makes it a good
fit when nobody knows yet how long something needs to be kept. Legal holds exist in buckets with
Object Lock enabled and are set per object version - there is no bucket
default for them. GetObjectLegalHold reads a hold back.
- Each bucket is reached through its own regional endpoint, and a bucket in another region
answers
404 NoSuchBucketrather than a redirect. x-amz-expected-bucket-owneris welcome and has nothing to check: a key only ever reaches buckets of its own account.- The request body is sent in one piece: an
aws-chunkedbody is answered501 NotImplemented. - A key with a
.or..path segment (a/../b) cannot be put in a URL, so STRG answers400 InvalidURIfor it.
When you'd use it
- A dispute or an investigation. When a team is asked to preserve a set of contracts or emails, a script places a hold on each version involved, and lifts the holds once the matter is closed - however long that turns out to take.
- Pausing a clean-up for one object. A bucket whose old versions a lifecycle rule or a nightly job removes can keep a single version safe from both by holding it, without changing the rule.
- An agent keeping evidence. An agent asked to make sure a report is not deleted while it is being reviewed places a hold on that version, and the reviewer, or the agent later on, lifts it.
Request
PUT /photos/2024/cat.jpg?legal-hold HTTP/1.1
Host: my-bucket.s3.eu-central-1.strg.com
Authorization: AWS4-HMAC-SHA256 Credential=...
x-amz-date: 20260926T120000Z
x-amz-content-sha256: <SHA-256 of the body>
Content-Type: application/xml
Content-MD5: <base64 MD5 of the body>
<?xml version="1.0" encoding="UTF-8"?>
<LegalHold xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Status>ON</Status>
</LegalHold>
The path-style form is PUT /my-bucket/photos/2024/cat.jpg?legal-hold on
s3.eu-central-1.strg.com - Addressing explains both.
Permissions
The access key needs Set legal holds (object-legal-hold:write). A key limited to certain
buckets can hold it too, and then sets legal holds in those buckets; a key limited to a key prefix
sets them for objects under that prefix. Access keys has the details.
URI parameters
| Name | Type | Required | Description |
|---|---|---|---|
Bucket | String | Yes | The bucket holding the object. |
Key | String | Yes | The object's key. A key with a . or .. path segment (a/../b) is answered 400 InvalidURI. |
legal-hold | Flag | Yes | Selects the legal hold subresource. Sent with no value. |
versionId | String | No | The version to hold or release. Without it, the current version. |
A query parameter other than these is answered 501 NotImplemented, so a client knows straight
away; presigning's X-Amz-* parameters and the JavaScript SDK's x-id are always welcome.
Request headers
| Name | Type | Required | Description |
|---|---|---|---|
Content-MD5 | String | No | Base64 MD5 of the body. When it is sent, STRG checks it, and a mismatch is answered 400 BadDigest. The SDKs compute an integrity header on their own. |
x-amz-sdk-checksum-algorithm | String | No | Set by the SDKs to name the checksum they send with the body. |
x-amz-expected-bucket-owner | String | No | Welcome, with nothing to check: an access key only ever reaches its own account's buckets. |
x-amz-request-payer | String | No | Not applicable: STRG has no Requester Pays buckets. |
Request body
An XML document of up to 2 MiB; a larger one is answered 400 MaxMessageLengthExceeded.
| Element | Type | Required | Description |
|---|---|---|---|
LegalHold | Container | Yes | The root element. |
Status | String | Yes | ON to place the hold, OFF to lift it. |
Response
A successful request answers 200 OK with an empty body.
Response headers
| Name | Description |
|---|---|
x-amz-request-id | The request ID, handy for a support request. |
x-amz-request-charged | Not sent: STRG has no Requester Pays buckets. |
Response body
None.
Errors
These are the errors this request can return, and the situation behind each one.
| Code | HTTP status | When |
|---|---|---|
NoSuchBucket | 404 | The bucket does not exist, it is in a region other than the endpoint's, or the account has no storage in this endpoint's region. |
AccessDenied | 403 | The access key lacks Set legal holds, or the bucket or key is outside its buckets or prefix. |
InvalidAccessKeyId | 403 | The access key does not exist, has expired or been revoked - or it was created moments ago and is not usable yet in a region where the account already stores data, in which case the same request works shortly afterwards. |
InvalidURI | 400 | The key contains a . or .. path segment. |
SignatureDoesNotMatch | 403 | The request signature does not match. |
RequestTimeTooSkewed | 403 | The request time is more than 15 minutes from the server's. |
AuthorizationHeaderMalformed | 400 | The signature's credential scope names a region other than the endpoint's. |
BadDigest | 400 | Content-MD5 does not match the body. |
XAmzContentSHA256Mismatch | 400 | x-amz-content-sha256 does not match the body. |
MalformedXML | 400 | The body is not valid UTF-8. |
MaxMessageLengthExceeded | 400 | The body is larger than 2 MiB. |
NotImplemented | 501 | The request carries a query parameter other than legal-hold and versionId (presigning's X-Amz-* parameters and the JavaScript SDK's x-id aside), or an aws-chunked body. |
SlowDown | 503 | Too many requests. The Retry-After header says when a retry is welcome; the AWS SDKs retry on their own. |
ServiceUnavailable | 503 | A temporary failure; the same request can be retried. |
In a bucket without Object Lock there are no legal holds to set, so the request answers with a
4xx error and changes nothing.
Differences from Amazon S3
The table lists each place where STRG behaves differently, and what that means for a client.
| Aspect | Amazon S3 | STRG |
|---|---|---|
| Bucket in another region | 301 redirect with x-amz-bucket-region | 404 NoSuchBucket. A bucket is reached through its own regional endpoint |
| Nothing stored in the endpoint's region yet | Any valid key reaches the endpoint | 404 NoSuchBucket |
x-amz-expected-bucket-owner | Checked against the bucket owner | Welcome, with nothing to check |
aws-chunked request body | Accepted | 501 NotImplemented: the body is sent in one piece |
Keys with . or .. segments | Allowed | 400 InvalidURI: such a key cannot be put in a URL, so its legal hold cannot be set |
Nobody can delete a version under a legal hold until the hold is lifted, and until then it is
billed as stored data. A hold that is no longer needed ends with Status OFF, after which the
version can be deleted as usual - unless a retention period still covers it.
Examples
Placing a legal hold on the current version of photos/2024/cat.jpg:
- AWS CLI
- Python (boto3)
- JavaScript (AWS SDK v3)
aws s3api put-object-legal-hold \
--bucket my-bucket \
--key photos/2024/cat.jpg \
--legal-hold Status=ON \
--endpoint-url https://s3.eu-central-1.strg.com \
--region eu-central-1
import boto3
s3 = boto3.client(
"s3",
endpoint_url="https://s3.eu-central-1.strg.com",
region_name="eu-central-1",
)
s3.put_object_legal_hold(
Bucket="my-bucket",
Key="photos/2024/cat.jpg",
LegalHold={"Status": "ON"},
)
import { PutObjectLegalHoldCommand, S3Client } from "@aws-sdk/client-s3";
const client = new S3Client({
endpoint: "https://s3.eu-central-1.strg.com",
region: "eu-central-1",
});
await client.send(
new PutObjectLegalHoldCommand({
Bucket: "my-bucket",
Key: "photos/2024/cat.jpg",
LegalHold: { Status: "ON" },
}),
);
Lifting the legal hold on a specific version, whose ID ListObjectVersions lists:
- AWS CLI
- Python (boto3)
- JavaScript (AWS SDK v3)
aws s3api put-object-legal-hold \
--bucket my-bucket \
--key photos/2024/cat.jpg \
--version-id "$VERSION_ID" \
--legal-hold Status=OFF \
--endpoint-url https://s3.eu-central-1.strg.com \
--region eu-central-1
import os
import boto3
s3 = boto3.client(
"s3",
endpoint_url="https://s3.eu-central-1.strg.com",
region_name="eu-central-1",
)
s3.put_object_legal_hold(
Bucket="my-bucket",
Key="photos/2024/cat.jpg",
VersionId=os.environ["VERSION_ID"],
LegalHold={"Status": "OFF"},
)
import { PutObjectLegalHoldCommand, S3Client } from "@aws-sdk/client-s3";
const client = new S3Client({
endpoint: "https://s3.eu-central-1.strg.com",
region: "eu-central-1",
});
await client.send(
new PutObjectLegalHoldCommand({
Bucket: "my-bucket",
Key: "photos/2024/cat.jpg",
VersionId: process.env.VERSION_ID,
LegalHold: { Status: "OFF" },
}),
);
An object can also be uploaded already under a legal hold: PutObject,
CopyObject and CreateMultipartUpload take
x-amz-object-lock-legal-hold: ON, which needs Set legal holds as well.