GetObject
GetObject downloads an object. Without anything extra it returns the current version, whole;
Range or partNumber returns part of it, and versionId returns an earlier version. When only
the metadata is needed, HeadObject answers the same question without the body.
Someone without an access key can download an object too, in one of two ways: through a presigned URL - Sharing an object with a presigned URL shows how - or from a bucket whose Bucket Protection is switched off, which makes every object in it readable without credentials, as Reading from a public bucket describes.
- An unsigned read of a public bucket can be answered from a cache for up to 5 minutes -
less if the object's own
Cache-Controlasks for less. That keeps popular public files quick, and it also means that for up to 5 minutes after an object is overwritten or deleted, Bucket Protection is switched back on, or the bucket's CORS rules change, unsigned readers can still get the earlier answer. Signed requests always read the stored object. Caching of unsigned reads has the details. response-content-dispositionand the otherresponse-*values are Latin-1 text; a value with other characters is answered500 InternalError. Thefilename*=UTF-8''…form carries a file name in any script.- A key with a
.or..path segment (a/../b) cannot travel in a URL intact, so STRG answers400 InvalidURIto a request for one, signed or unsigned. x-amz-expected-bucket-owneris welcome and has nothing to check: a key only ever reaches buckets of its own account.
When you'd use it
- Downloads in an app. A web app lets its users download their invoices or photos. The
server makes a presigned GetObject URL with
response-content-disposition, and the browser fetches the file straight from STRG and saves it under a friendly name. - Streaming and seeking. A video player or a tool reading a large archive asks for just the
bytes it needs with
Range, so seeking to minute 40 does not mean downloading the first 39. - Restoring an earlier version. Every STRG bucket keeps versions, so a backup job or a
person who overwrote a file by mistake can read the version from yesterday by its
versionId. - Public files and agents. A website serves its images from a public bucket with no credentials at all, and an agent that keeps its notes in a bucket reads them back at the start of every session.
Request
GET /photos/2024/cat.jpg 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: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Range: bytes=0-1023
The path-style form is GET /my-bucket/photos/2024/cat.jpg on s3.eu-central-1.strg.com,
which a bucket name containing a dot needs - Addressing explains
both.
Permissions
The access key needs Read objects (objects:read), and the same permission reads the
current version and every earlier one. For a key limited to certain buckets or to a key prefix,
the bucket and the object key are within them. Access keys has the details.
An unsigned request needs no access key at all, and is answered for a bucket whose
Bucket Protection is off. For a protected bucket the answer is 403 AccessDenied.
URI parameters
| Name | Type | Required | Description |
|---|---|---|---|
Bucket | String | Yes | The bucket holding the object. |
Key | String | Yes | The object key. A key containing a . or .. path segment is answered 400 InvalidURI. |
versionId | String | No | The version to read; without it, the current version. Version IDs come from ListObjectVersions and from the x-amz-version-id response header, and are opaque strings. |
partNumber | Integer | No | Returns just this part of an object uploaded with multipart upload, 1-10,000. The response is 206 Partial Content. |
response-cache-control | String | No | Sets Cache-Control on the response. |
response-content-disposition | String | No | Sets Content-Disposition on the response - attachment; filename="cat.jpg", for example, makes a browser save the file. The value is Latin-1 text; the filename*=UTF-8''… form carries other characters. |
response-content-encoding | String | No | Sets Content-Encoding on the response. |
response-content-language | String | No | Sets Content-Language on the response. |
response-content-type | String | No | Sets Content-Type on the response. |
response-expires | String | No | Sets Expires on the response. |
The response-* parameters apply to a 200 or 206 answer, never to a 304 or an error.
They belong to signed requests and presigned URLs: an unsigned request carrying one is answered
400 InvalidRequest, just as on Amazon S3. Any other query parameter - a presigned URL's own
X-Amz-* parameters aside - is answered 501 NotImplemented, so a client learns straight away
that it has no effect here.
Request headers
| Name | Type | Required | Description |
|---|---|---|---|
Range | String | No | One byte range per request, as on Amazon S3: bytes=<first>-<last>, bytes=<first>- or bytes=-<suffix length>. The response is 206 Partial Content. |
If-Match | String | No | Returns the object only if its ETag matches; otherwise 412 Precondition Failed. |
If-None-Match | String | No | Returns the object only if its ETag differs; otherwise 304 Not Modified. |
If-Modified-Since | Timestamp | No | Returns the object only if it changed after this time; otherwise 304 Not Modified. |
If-Unmodified-Since | Timestamp | No | Returns the object only if it has not changed since this time; otherwise 412 Precondition Failed. |
x-amz-server-side-encryption-customer-algorithm | String | For SSE-C | AES256, for an object stored with a customer-provided key (SSE-C). |
x-amz-server-side-encryption-customer-key | String | For SSE-C | The base64-encoded 256-bit key the object was stored with. |
x-amz-server-side-encryption-customer-key-MD5 | String | For SSE-C | The base64-encoded MD5 digest of that key. |
x-amz-checksum-mode | String | No | ENABLED asks for the object's stored checksum in the response. |
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 | Has no effect: STRG has no Requester Pays buckets. |
On a signed request, every x-amz-* header is part of the signature -
Authentication explains how.
Request body
None.
Response
A successful read answers 200 OK with the object as the body, or 206 Partial Content for a
Range or partNumber request.
Response headers
These headers describe the object and the part of it the body holds.
| Name | Description |
|---|---|
Content-Length | The size of the body in bytes. |
Content-Type | The object's media type, or the response-content-type value. |
Content-Range | For a 206: the bytes returned and the object's total size, bytes 0-1023/52431. |
Accept-Ranges | bytes. |
ETag | The object's entity tag. |
Last-Modified | When this version was written. |
Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Expires | As stored with the object, or the matching response-* value. |
x-amz-meta-* | The object's user-defined metadata. |
x-amz-version-id | The version returned. |
x-amz-delete-marker | true, on a 404, when the object's current version is a delete marker. |
x-amz-mp-parts-count | With partNumber: how many parts the object has. |
x-amz-checksum-crc32, x-amz-checksum-crc32c, x-amz-checksum-crc64nvme, x-amz-checksum-sha1, x-amz-checksum-sha256, x-amz-checksum-type | The object's stored checksum, where one was stored. Never sent with a 304, which has no body to check. |
x-amz-server-side-encryption | AES256 for an object encrypted with STRG-managed keys. |
x-amz-server-side-encryption-customer-algorithm, x-amz-server-side-encryption-customer-key-MD5 | For an SSE-C object, confirming the key that was sent. |
x-amz-object-lock-mode, x-amz-object-lock-retain-until-date | The version's retention, included when the access key also has Read retention. Object Lock explains retention. |
x-amz-object-lock-legal-hold | The version's legal hold, included when the access key also has Read legal holds. |
x-amz-request-id, x-amz-id-2 | The request ID, useful when asking support about a request. |
Access-Control-Allow-Origin, Access-Control-Allow-Credentials, Access-Control-Expose-Headers, Vary | For a browser request whose Origin the bucket's CORS rules allow, signed or unsigned. CORS rules has the details. |
x-amz-storage-class is absent or STANDARD: every object is STANDARD. Some other S3 response
headers have nothing to describe on STRG, so they never appear: x-amz-tagging-count (objects carry no tags), x-amz-website-redirect-location,
x-amz-replication-status, x-amz-restore, x-amz-missing-meta, x-amz-request-charged,
x-amz-checksum-md5, x-amz-checksum-sha512 and x-amz-checksum-xxhash3, -xxhash64 and
-xxhash128 (uploads store the five checksum algorithms listed above), the SSE-KMS headers
(x-amz-server-side-encryption-aws-kms-key-id,
x-amz-server-side-encryption-bucket-key-enabled) and the Object Lock event hold headers
(x-amz-object-lock-event-hold*). x-amz-expiration is not a dependable guide to when an
object expires; the bucket's lifecycle rules are.
Response body
The object's bytes, exactly as stored - or the requested range or part of them.
Caching of unsigned reads
An unsigned GetObject of a public bucket's object can be answered from an edge cache rather than from storage, which makes popular public files quick to fetch. A signed request or a presigned URL always reads the stored object. This is how the cache decides:
| What is cached | A 200 answer whose Cache-Control lets a shared cache keep it: s-maxage - or, without one, max-age - greater than 0, and none of no-store, no-cache or private. An object stored without such a Cache-Control is never cached. The Cache-Control header of PutObject sets it |
| For how long | The shorter of that lifetime and 5 minutes. The response still carries the object's own Cache-Control, which browsers and other caches follow |
| From the cache | Range requests (206), and If-None-Match and If-Modified-Since (304) |
| Always from storage | A request carrying If-Match, If-Unmodified-Since, If-Range or any x-amz-* header, or a query parameter other than versionId |
| After a change | The cache is never cleared early. For up to 5 minutes - or the object's shorter lifetime - unsigned readers can still get the earlier answer after the object is overwritten or deleted, after Bucket Protection is switched back on, or after the bucket's CORS rules change. An object that changes often does well with a short max-age or s-maxage |
Errors
These are the answers a read can get instead of the object, and the situation behind each.
| Code | HTTP status | When |
|---|---|---|
| - | 304 | If-None-Match or If-Modified-Since did not match. Not an error: the SDKs report it as not modified. |
PreconditionFailed | 412 | If-Match or If-Unmodified-Since did not match. |
InvalidRange | 416 | The Range starts beyond the end of the object. |
NoSuchKey | 404 | No object has this key, or its current version is a delete marker. |
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. An unsigned request can also get it for a few seconds after a bucket is created. |
AccessDenied | 403 | The access key lacks Read objects, or the bucket or key is outside its buckets or prefix. Also an expired presigned URL, an unsigned x-amz-* header, and an unsigned request to a bucket whose Bucket Protection is on. |
InvalidRequest | 400 | An unsigned request carries a response-* parameter. |
InvalidURI | 400 | The object key contains a . or .. path segment, signed or unsigned. |
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. Until then the dashboard's Access Keys page shows Setting up in … for that region, and Ready in … once it is done. |
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. |
NotImplemented | 501 | The request carries a query parameter GetObject does not take. |
InternalError | 500 | A response-* value contains characters outside Latin-1. |
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. |
Differences from Amazon S3
The table lists each place where STRG behaves differently, and what that means for a client.
| Aspect | Amazon S3 | STRG |
|---|---|---|
| Unsigned reads of a public object | Read from storage | Can be served from a cache for up to 5 minutes, or the object's shorter Cache-Control lifetime - including after it was overwritten or deleted, Bucket Protection was switched on, or the CORS rules changed. A signed request always reads the stored object |
response-* values | Any value S3 accepts | Latin-1 text; other characters are answered 500 InternalError. filename*=UTF-8''… in response-content-disposition carries any file name |
| Bucket in another region | 301 redirect with x-amz-bucket-region | 404 NoSuchBucket. A bucket is reached through its own regional endpoint |
Keys with . or .. segments | Allowed | 400 InvalidURI |
| Unknown query parameters | Ignored | 501 NotImplemented, so a client learns straight away that the parameter has no effect |
x-amz-expected-bucket-owner | Checked against the bucket owner | Welcome, with nothing to check |
| Object Lock headers | Returned with s3:GetObjectRetention / s3:GetObjectLegalHold | Returned with Read retention / Read legal holds |
Examples
Downloading photos/2024/cat.jpg to a local file:
- AWS CLI
- Python (boto3)
- JavaScript (AWS SDK v3)
aws s3api get-object \
--bucket my-bucket \
--key photos/2024/cat.jpg \
--endpoint-url https://s3.eu-central-1.strg.com \
--region eu-central-1 \
cat.jpg
import boto3
s3 = boto3.client(
"s3",
endpoint_url="https://s3.eu-central-1.strg.com",
region_name="eu-central-1",
)
response = s3.get_object(Bucket="my-bucket", Key="photos/2024/cat.jpg")
with open("cat.jpg", "wb") as f:
for chunk in response["Body"].iter_chunks():
f.write(chunk)
print(response["ContentType"], response["ContentLength"], response["VersionId"])
import { writeFile } from "node:fs/promises";
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
const client = new S3Client({
endpoint: "https://s3.eu-central-1.strg.com",
region: "eu-central-1",
});
const response = await client.send(
new GetObjectCommand({ Bucket: "my-bucket", Key: "photos/2024/cat.jpg" }),
);
await writeFile("cat.jpg", await response.Body.transformToByteArray());
console.log(response.ContentType, response.ContentLength, response.VersionId);
Reading a byte range
The first 1,024 bytes of the object:
- AWS CLI
- Python (boto3)
- JavaScript (AWS SDK v3)
aws s3api get-object \
--bucket my-bucket \
--key photos/2024/cat.jpg \
--range bytes=0-1023 \
--endpoint-url https://s3.eu-central-1.strg.com \
--region eu-central-1 \
cat-head.bin
import boto3
s3 = boto3.client(
"s3",
endpoint_url="https://s3.eu-central-1.strg.com",
region_name="eu-central-1",
)
response = s3.get_object(
Bucket="my-bucket",
Key="photos/2024/cat.jpg",
Range="bytes=0-1023",
)
data = response["Body"].read()
print(response["ContentRange"], len(data))
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
const client = new S3Client({
endpoint: "https://s3.eu-central-1.strg.com",
region: "eu-central-1",
});
const response = await client.send(
new GetObjectCommand({
Bucket: "my-bucket",
Key: "photos/2024/cat.jpg",
Range: "bytes=0-1023",
}),
);
const data = await response.Body.transformToByteArray();
console.log(response.ContentRange, data.length);
Reading an earlier version
Here the version ID, from ListObjectVersions, is in VERSION_ID:
- AWS CLI
- Python (boto3)
- JavaScript (AWS SDK v3)
aws s3api get-object \
--bucket my-bucket \
--key photos/2024/cat.jpg \
--version-id "$VERSION_ID" \
--endpoint-url https://s3.eu-central-1.strg.com \
--region eu-central-1 \
cat-previous.jpg
import os
import boto3
s3 = boto3.client(
"s3",
endpoint_url="https://s3.eu-central-1.strg.com",
region_name="eu-central-1",
)
response = s3.get_object(
Bucket="my-bucket",
Key="photos/2024/cat.jpg",
VersionId=os.environ["VERSION_ID"],
)
with open("cat-previous.jpg", "wb") as f:
f.write(response["Body"].read())
import { writeFile } from "node:fs/promises";
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
const client = new S3Client({
endpoint: "https://s3.eu-central-1.strg.com",
region: "eu-central-1",
});
const response = await client.send(
new GetObjectCommand({
Bucket: "my-bucket",
Key: "photos/2024/cat.jpg",
VersionId: process.env.VERSION_ID,
}),
);
await writeFile("cat-previous.jpg", await response.Body.transformToByteArray());
Reading from a public bucket
Once a bucket's Bucket Protection is off, anyone can read its objects without credentials, at
https://s3.eu-central-1.strg.com/my-bucket/photos/2024/cat.jpg or
https://my-bucket.s3.eu-central-1.strg.com/photos/2024/cat.jpg:
curl -O https://my-bucket.s3.eu-central-1.strg.com/photos/2024/cat.jpg
The SDKs send an unsigned request when they are configured not to sign:
- AWS CLI
- Python (boto3)
- JavaScript (AWS SDK v3)
aws s3api get-object \
--bucket my-bucket \
--key photos/2024/cat.jpg \
--no-sign-request \
--endpoint-url https://s3.eu-central-1.strg.com \
--region eu-central-1 \
cat.jpg
import boto3
from botocore import UNSIGNED
from botocore.config import Config
s3 = boto3.client(
"s3",
endpoint_url="https://s3.eu-central-1.strg.com",
region_name="eu-central-1",
config=Config(signature_version=UNSIGNED),
)
response = s3.get_object(Bucket="my-bucket", Key="photos/2024/cat.jpg")
with open("cat.jpg", "wb") as f:
f.write(response["Body"].read())
import { writeFile } from "node:fs/promises";
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
const client = new S3Client({
endpoint: "https://s3.eu-central-1.strg.com",
region: "eu-central-1",
// Sends every request unsigned.
signer: { sign: async (request) => request },
});
const response = await client.send(
new GetObjectCommand({ Bucket: "my-bucket", Key: "photos/2024/cat.jpg" }),
);
await writeFile("cat.jpg", await response.Body.transformToByteArray());
Sharing an object with a presigned URL
A presigned URL lets anyone who has it download the object until it expires, with no access key of their own. It carries the permissions of the key that signed it, so that key needs Read objects. The expiry can be anything from 1 second to 7 days (604,800 seconds), and revoking the signing key stops the URL working within 60 seconds.
This URL is valid for one hour and makes a browser save the file as cat.jpg:
- AWS CLI
- Python (boto3)
- JavaScript (AWS SDK v3)
aws s3 presign s3://my-bucket/photos/2024/cat.jpg \
--expires-in 3600 \
--endpoint-url https://s3.eu-central-1.strg.com \
--region eu-central-1
The AWS CLI's presign has no option for response-* parameters; the SDKs, in the other two
tabs, can add them.
import boto3
s3 = boto3.client(
"s3",
endpoint_url="https://s3.eu-central-1.strg.com",
region_name="eu-central-1",
)
url = s3.generate_presigned_url(
"get_object",
Params={
"Bucket": "my-bucket",
"Key": "photos/2024/cat.jpg",
"ResponseContentDisposition": 'attachment; filename="cat.jpg"',
},
ExpiresIn=3600,
)
print(url)
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const client = new S3Client({
endpoint: "https://s3.eu-central-1.strg.com",
region: "eu-central-1",
});
const url = await getSignedUrl(
client,
new GetObjectCommand({
Bucket: "my-bucket",
Key: "photos/2024/cat.jpg",
ResponseContentDisposition: 'attachment; filename="cat.jpg"',
}),
{ expiresIn: 3600 },
);
console.log(url);
getSignedUrl is in the @aws-sdk/s3-request-presigner package.