# HeadObject

HeadObject returns an object's headers without its body: the same answer as
[GetObject](https://docs.strg.com/s3-api/GetObject), minus the data. It is the quick way to find out whether an object
exists, how big it is, its `ETag`, content type and `x-amz-meta-*` metadata, or whether a
condition holds before a download starts - all without transferring a single byte of the object.

**Info: Good to know on STRG**

- Every object is stored in `STANDARD`, so there is no storage class to keep track of.
- The `response-*` overrides take Latin-1 text; a value outside it answers `500 InternalError`,
  and the `filename*=UTF-8''...` form carries any other file name.
- Anyone can read the metadata of an object in a bucket whose *Bucket Protection* is off,
  without signing - `response-*` overrides need a signed request or a presigned URL.
- An unsigned request can be answered from a cache for up to 5 minutes, when the object's own
  `Cache-Control` allows it.
- A key with a `.` or `..` path segment (`a/../b`) cannot be put in a URL, so it answers
  `400 InvalidURI`.
- A bucket in another region answers `404`, rather than a `301` redirect.
- `x-amz-expected-bucket-owner` is welcome and has nothing to check: a key only ever reaches
  buckets of its own account.
- A query parameter HeadObject does not take answers `501 NotImplemented`, so a client knows
  straight away.

## When you'd use it

- **A sync or backup tool deciding what to upload.** Before sending a file, a nightly backup
  compares the stored object's size, `ETag` and `Last-Modified` with the local copy, and
  uploads only what changed.
- **Checking that an object is there.** A web app that has just received an upload confirms the
  object exists before showing its link; a missing one answers `404`.
- **Planning a large download.** A download manager reads `Content-Length` - and, with
  `partNumber`, `x-amz-mp-parts-count` - and then fetches the object in ranges or parts with
  [GetObject](https://docs.strg.com/s3-api/GetObject), several at once.
- **An agent reading its own notes.** An agent keeping files in a bucket stores a summary in
  `x-amz-meta-*` metadata, and reads it back with HeadObject without downloading each file.

## Request

```http
HEAD /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
```

The path-style form is `HEAD /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. A presigned `HEAD` URL works too.

### Permissions

The access key needs **Read objects** (`objects:read`). For a key limited to certain buckets or
to a key prefix, the bucket and the object key are within them; anything else answers
`403 AccessDenied`. [Access keys](https://docs.strg.com/access-keys) has the details.

The Object Lock headers appear in the response when the key also holds **Read retention**
(`object-retention:read`), for the retention headers, or **Read legal holds**
(`object-legal-hold:read`), for the legal hold header. Without them the response simply leaves
the object's lock out. [Object Lock](https://docs.strg.com/object-locks) explains both.

### Unsigned requests

A request with no signature at all - no `Authorization` header and no presigned query - reads
the metadata of an object in a bucket whose *Bucket Protection* is off, just as
[GetObject](https://docs.strg.com/s3-api/GetObject) reads the object. A protected bucket answers `403`, and a bucket
that does not exist, or is in another region, `404`. `response-*` overrides need a signature, so
an unsigned request carrying one answers `400`. [Anonymous reads](https://docs.strg.com/s3-api/overview#anonymous-reads)
covers every case.

An unsigned `HEAD` can be answered from a cache rather than from the stored object, when the
object was stored with a `Cache-Control` that lets a shared cache keep it (`max-age` or
`s-maxage` above zero, and none of `no-store`, `no-cache` or `private`). A cached answer is kept
for the shorter of that lifetime and **5 minutes**, and the response still carries the object's
own `Cache-Control`. The cache is never cleared early: for up to 5 minutes after an object is
overwritten or deleted, *Bucket Protection* is turned on, or the bucket's
[CORS rules](https://docs.strg.com/cors-rules) change, anonymous readers can still get the previous answer. An
object stored without `Cache-Control` is never cached, and a request carrying `If-Match`,
`If-Unmodified-Since` or any `x-amz-*` header, or a query parameter other than `versionId`,
always reads the stored object.

A bucket created a moment ago can answer unsigned requests with `404` for a few seconds, until
every endpoint has heard about it.

### URI parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Bucket` | String | Yes | The bucket that holds the object. |
| `Key` | String | Yes | The object's key. A key with a `.` or `..` path segment (`a/../b`) cannot be put in a URL and answers `400 InvalidURI`. |
| `versionId` | String | No | A specific version of the object. Without it, the current version is used. |
| `partNumber` | Integer | No | 1-10,000. Describes that part of an object uploaded by multipart upload: `Content-Length` is the part's size. |
| `response-cache-control` | String | No | Sets `Cache-Control` on the response. |
| `response-content-disposition` | String | No | Sets `Content-Disposition` on the response. Latin-1 characters only - a value outside it answers `500 InternalError` - and the `filename*=UTF-8''...` form carries a non-ASCII file name. |
| `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-*` overrides apply to a `200` or `206` answer only, never to a `304` or an error,
and need a signed request or a presigned URL: an unsigned request carrying one answers `400`.
Any other query parameter, apart from presigning's own `X-Amz-*`, answers `501 NotImplemented`.

### Request headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `If-Match` | String | No | Answers only if the object's `ETag` matches; otherwise `412`. |
| `If-None-Match` | String | No | Answers only if the object's `ETag` differs; otherwise `304`. |
| `If-Modified-Since` | Timestamp | No | Answers only if the object changed after this time; otherwise `304`. |
| `If-Unmodified-Since` | Timestamp | No | Answers only if the object has not changed since this time; otherwise `412`. |
| `Range` | String | No | A byte range, such as `bytes=0-1023`. Only the headers change: `Content-Length` and `Content-Range` describe the range. A range that cannot be satisfied answers `416`. |
| `x-amz-server-side-encryption-customer-algorithm` | String | For SSE-C objects | `AES256`. Required for an object stored with a customer-provided key (SSE-C). |
| `x-amz-server-side-encryption-customer-key` | String | For SSE-C objects | The base64-encoded 256-bit key the object was stored with. |
| `x-amz-server-side-encryption-customer-key-MD5` | String | For SSE-C objects | The base64-encoded MD5 digest of that key. |
| `x-amz-checksum-mode` | String | No | `ENABLED` asks for the object's stored checksum. |
| `x-amz-request-payer` | String | No | Has no effect: STRG has no Requester Pays buckets, so there is never a need to send it. |
| `x-amz-expected-bucket-owner` | String | No | Welcome, with nothing to check: an access key only ever reaches its own account's buckets. |

A signed request signs every `x-amz-*` header it carries; one left unsigned answers `403`.
[Authentication](https://docs.strg.com/s3-api/overview#authentication) has the details.

### Request body

None.

## Response

A `HEAD` response never has a body, successful or not.

### Response headers

| Name | Description |
| --- | --- |
| `Content-Length` | The object's size in bytes - or the part's with `partNumber`, or the range's with `Range`. |
| `Content-Type`, `Cache-Control`, `Content-Disposition`, `Content-Encoding`, `Content-Language`, `Expires` | As stored with the object, or as set by a `response-*` override. |
| `Content-Range` | With `Range`, the range the answer describes. |
| `Accept-Ranges` | `bytes`. |
| `ETag` | The object's entity tag. |
| `Last-Modified` | When the object was last written. |
| `x-amz-meta-*` | The object's user metadata, one header per entry. |
| `x-amz-version-id` | The version described. |
| `x-amz-delete-marker` | `true` when the version addressed is a delete marker. |
| `x-amz-server-side-encryption` | `AES256` when the object is 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, echoing the key you sent. |
| `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 checksum stored with the object, if it was uploaded with one. Never on a `304`. |
| `x-amz-checksum-sha512`, `x-amz-checksum-md5`, `x-amz-checksum-xxhash64`, `x-amz-checksum-xxhash3`, `x-amz-checksum-xxhash128` | Not returned: uploads use the checksum algorithms in the row above. |
| `x-amz-mp-parts-count` | With `partNumber`, the number of parts in a multipart object. |
| `x-amz-storage-class` | Absent or `STANDARD`: every object is `STANDARD`. |
| `x-amz-object-lock-mode`, `x-amz-object-lock-retain-until-date` | The object's retention, when it has one and the key holds *Read retention*. |
| `x-amz-object-lock-legal-hold` | `ON` or `OFF`, when the key holds *Read legal holds*. |
| `x-amz-object-lock-event-hold`, `x-amz-object-lock-event-hold-duration-days`, `x-amz-object-lock-event-hold-duration-years` | Not returned: STRG has no event holds. |
| `x-amz-tagging-count` | Not returned: objects carry no tags. |
| `x-amz-server-side-encryption-aws-kms-key-id`, `x-amz-server-side-encryption-bucket-key-enabled` | Not returned: STRG encrypts with its own managed keys (`AES256`) or yours (SSE-C), never SSE-KMS. |
| `x-amz-restore`, `x-amz-archive-status` | Not returned: there are no archive storage classes, so every object is ready to read. |
| `x-amz-replication-status` | Not returned: STRG has no bucket replication. |
| `x-amz-website-redirect-location` | Not returned: STRG has no website hosting. |
| `x-amz-expiration` | Not guaranteed when a [lifecycle policy](https://docs.strg.com/lifecycle-policies) applies, so a client is better off not depending on it. |
| `x-amz-missing-meta` | Not returned. |
| `x-amz-request-charged` | Not returned: STRG has no Requester Pays buckets. |
| `x-amz-request-id` | The request ID, handy for a support 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. |

### Response body

None.

## Errors

A `HEAD` error has no body, so only the HTTP status reaches your client: the AWS SDK for
JavaScript reports a `404` as `NotFound`, and boto3 raises a `ClientError` whose code is the
status (`"404"`). The table names the S3 error behind each status, and the situation it comes
from.

| Code | HTTP status | When |
| --- | --- | --- |
| - | 304 | `If-None-Match` matched, or the object is unchanged since `If-Modified-Since`. Not an error: the object simply has not changed. |
| `PreconditionFailed` | 412 | `If-Match` did not match, or the object changed since `If-Unmodified-Since`. |
| `InvalidRange` | 416 | The `Range` cannot be satisfied. |
| `NoSuchKey` | 404 | The object or version does not exist, or the 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 yet. |
| `AccessDenied` | 403 | The access key lacks *Read objects*, the bucket or key is outside its buckets or prefix, the account is not active, or an `x-amz-*` header of a signed request is unsigned. For an unsigned request: the bucket's *Bucket Protection* is on, or the bucket's owner has an invoice at the stage where every operation pauses. |
| `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. |
| `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. |
| `InvalidRequest` | 400 | An unsigned request carries a `response-*` override. |
| `InvalidURI` | 400 | The key contains a `.` or `..` path segment, signed or unsigned. |
| - | 400 | An unsigned request names a bucket deleted moments ago, or the request is otherwise malformed. |
| `InternalError` | 500 | A `response-*` override carries a character outside Latin-1. |
| `NotImplemented` | 501 | The request carries a query parameter HeadObject does not take. |
| `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

A few things work a little differently on STRG, each listed here with what it means for your
requests.

| Aspect | Amazon S3 | STRG |
| --- | --- | --- |
| Storage class | Several; the header names any but `STANDARD` | `STANDARD` for every object |
| Bucket in another region | `301` redirect with `x-amz-bucket-region` | `404`. A bucket is reached through its own regional endpoint |
| Anonymous requests | Allowed on a public object | Allowed in a bucket whose *Bucket Protection* is off; `403` otherwise |
| Anonymous answers | Always the stored object | Can come from a cache for the shorter of the object's `Cache-Control` lifetime and 5 minutes, so for up to 5 minutes an overwritten or deleted object can be described as it was, and a bucket just protected can still answer |
| Keys with `.` or `..` segments | Allowed | `400 InvalidURI`. [DeleteObjects](https://docs.strg.com/s3-api/DeleteObjects) deletes such a key |
| `response-*` overrides | Any value | Latin-1, otherwise `500`. `filename*=UTF-8''...` carries a non-ASCII file name |
| Unknown query parameters | Ignored | `501 NotImplemented`, so a client knows straight away |
| Tags, SSE-KMS, event holds, archive and replication headers | Returned where they apply | Never returned, since STRG has none of these features to report on |
| `x-amz-expected-bucket-owner` | Checked against the bucket owner | Welcome, with nothing to check |

## Examples

Reading the metadata of `photos/2024/cat.jpg` in `my-bucket`, and telling a missing object apart
from other failures:

**AWS CLI**

```bash
aws s3api head-object \
  --bucket my-bucket \
  --key photos/2024/cat.jpg \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1
```

A missing object exits with an error naming `(404)` and `Not Found`.

**Python (boto3)**

```python
import boto3
from botocore.exceptions import ClientError

s3 = boto3.client(
    "s3",
    endpoint_url="https://s3.eu-central-1.strg.com",
    region_name="eu-central-1",
)

try:
    head = s3.head_object(Bucket="my-bucket", Key="photos/2024/cat.jpg")
    print(head["ContentLength"], head["ContentType"], head["ETag"], head["Metadata"])
except ClientError as error:
    if error.response["Error"]["Code"] == "404":
        print("no such object")
    else:
        raise
```

**JavaScript (AWS SDK v3)**

```javascript
import { HeadObjectCommand, S3Client } from "@aws-sdk/client-s3";

const client = new S3Client({
  endpoint: "https://s3.eu-central-1.strg.com",
  region: "eu-central-1",
});

try {
  const head = await client.send(
    new HeadObjectCommand({ Bucket: "my-bucket", Key: "photos/2024/cat.jpg" }),
  );
  console.log(head.ContentLength, head.ContentType, head.ETag, head.Metadata);
} catch (error) {
  if (error.name === "NotFound") console.log("no such object");
  else throw error;
}
```

A specific version takes `--version-id` (CLI), `VersionId=` (boto3) or `VersionId`
(JavaScript), with the version's ID.

Checking by its `ETag` whether an object changed since it was last seen. An unchanged object
answers `304`, which the SDKs raise as an error:

**AWS CLI**

```bash
aws s3api head-object \
  --bucket my-bucket \
  --key photos/2024/cat.jpg \
  --if-none-match '"85f30635602dc09bd85957a6e82a2c21"' \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1
```

**Python (boto3)**

```python
import boto3
from botocore.exceptions import ClientError

s3 = boto3.client(
    "s3",
    endpoint_url="https://s3.eu-central-1.strg.com",
    region_name="eu-central-1",
)

try:
    head = s3.head_object(
        Bucket="my-bucket",
        Key="photos/2024/cat.jpg",
        IfNoneMatch='"85f30635602dc09bd85957a6e82a2c21"',
    )
    print("changed, new ETag:", head["ETag"])
except ClientError as error:
    if error.response["Error"]["Code"] == "304":
        print("unchanged")
    else:
        raise
```

**JavaScript (AWS SDK v3)**

```javascript
import { HeadObjectCommand, S3Client } from "@aws-sdk/client-s3";

const client = new S3Client({
  endpoint: "https://s3.eu-central-1.strg.com",
  region: "eu-central-1",
});

try {
  const head = await client.send(
    new HeadObjectCommand({
      Bucket: "my-bucket",
      Key: "photos/2024/cat.jpg",
      IfNoneMatch: '"85f30635602dc09bd85957a6e82a2c21"',
    }),
  );
  console.log("changed, new ETag:", head.ETag);
} catch (error) {
  if (error.$metadata?.httpStatusCode === 304) console.log("unchanged");
  else throw error;
}
```

Reading the metadata of an object in a bucket whose *Bucket Protection* is off, without
credentials:

**AWS CLI**

```bash
aws s3api head-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
```

**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),
)

head = s3.head_object(Bucket="my-bucket", Key="photos/2024/cat.jpg")
print(head["ContentLength"], head["ContentType"], head["ETag"])
```

**JavaScript (AWS SDK v3)**

```javascript
import { HeadObjectCommand, S3Client } from "@aws-sdk/client-s3";

const client = new S3Client({
  endpoint: "https://s3.eu-central-1.strg.com",
  region: "eu-central-1",
  credentials: { accessKeyId: "", secretAccessKey: "" },
  signer: { sign: async (request) => request },
});

const head = await client.send(
  new HeadObjectCommand({ Bucket: "my-bucket", Key: "photos/2024/cat.jpg" }),
);
console.log(head.ContentLength, head.ContentType, head.ETag);
```

## Related

- [GetObject](https://docs.strg.com/s3-api/GetObject)
- [GetObjectRetention](https://docs.strg.com/s3-api/GetObjectRetention)
- [GetObjectLegalHold](https://docs.strg.com/s3-api/GetObjectLegalHold)
- [ListObjectVersions](https://docs.strg.com/s3-api/ListObjectVersions)
- [HeadBucket](https://docs.strg.com/s3-api/HeadBucket)
- [Access keys](https://docs.strg.com/access-keys)
- [S3 API overview](https://docs.strg.com/s3-api/overview)
