# 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](https://docs.strg.com/s3-api/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](https://docs.strg.com/s3-api/GetObject#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](https://docs.strg.com/s3-api/GetObject#reading-from-a-public-bucket) describes.

**Info: Good to know on STRG**

- 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-Control` asks 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](https://docs.strg.com/s3-api/GetObject#caching-of-unsigned-reads) has the details.
- `response-content-disposition` and the other `response-*` values are Latin-1 text; a value
  with other characters is answered `500 InternalError`. The `filename*=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
  answers `400 InvalidURI` to a request for one, signed or unsigned.
- `x-amz-expected-bucket-owner` is 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

```http
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](https://docs.strg.com/s3-api/overview#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](https://docs.strg.com/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](https://docs.strg.com/s3-api/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](https://docs.strg.com/s3-api/CreateMultipartUpload), 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](https://docs.strg.com/s3-api/overview#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](https://docs.strg.com/object-locks) 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](https://docs.strg.com/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](https://docs.strg.com/lifecycle-policies) 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](https://docs.strg.com/s3-api/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**

```bash
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
```

**Python (boto3)**

```python
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"])
```

**JavaScript (AWS SDK v3)**

```javascript
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**

```bash
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
```

**Python (boto3)**

```python
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))
```

**JavaScript (AWS SDK v3)**

```javascript
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](https://docs.strg.com/s3-api/ListObjectVersions), is in `VERSION_ID`:

**AWS CLI**

```bash
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
```

**Python (boto3)**

```python
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())
```

**JavaScript (AWS SDK v3)**

```javascript
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`:

```bash
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**

```bash
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
```

**Python (boto3)**

```python
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())
```

**JavaScript (AWS SDK v3)**

```javascript
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**

```bash
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.

**Python (boto3)**

```python
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)
```

**JavaScript (AWS SDK v3)**

```javascript
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.

## Related

- [HeadObject](https://docs.strg.com/s3-api/HeadObject)
- [PutObject](https://docs.strg.com/s3-api/PutObject)
- [ListObjectsV2](https://docs.strg.com/s3-api/ListObjectsV2)
- [ListObjectVersions](https://docs.strg.com/s3-api/ListObjectVersions)
- [CopyObject](https://docs.strg.com/s3-api/CopyObject)
- [Object Lock](https://docs.strg.com/object-locks)
- [Access keys](https://docs.strg.com/access-keys)
- [S3 API overview](https://docs.strg.com/s3-api/overview)
