When you upload a file to S3 with custom metadata (like fileName or uploadedAt), everything looks correct in the AWS Console. But when you try to read that metadata from your frontend using HeadObjectCommand, the data clearly exists on the cloud, yet the AWS SDK response returns empty Metadata object.
There's a resolved GitHub issue on one of the JavaScript AWS packages.
This is not an bug, the cause isn’t in the AWS SDK or your upload logic. It comes from the browser, and specifically how it handles cross-origin requests.
The rest of this article breaks down where this behavior comes from and how to fix it.
If you read my earlier article, “Stop Uploading Files to S3 the Wrong Way”, you saw a complete example of how to upload and download files efficiently using the AWS SDK with a client/server flow.
This article builds directly on top of that and acts as a follow-up for developers searching for “client-side S3 metadata not showing” issues.
Table of Contents
- This is Part 2 in a small S3 series
- Client and Server behave differently
- Why no Metadata on the Client
- How to Fix It with S3 CORS Rules
This is Part 2 in a small S3 series
In the first part, the focus was sending and retrieving files. Here, we look at reading custom metadata from a cloud S3 provider in the browser.
You don’t need to read Part 1 to follow this article, but both posts fit together if you’re building a file upload pipeline end-to-end.
Client and Server behave differently
The issue only shows up in certain execution environments, so it helps to look at how the same code behaves in your backend (when it runs on the server) versus in the browser.
Server-side (✅ works as expected)
When HeadObjectCommand runs in a nodejs backend environment - for example, a Next.js API route - it returns the full metadata without any issues. Server-to-server requests aren’t limited by browser security rules, so all response headers are available.
// src/app/api/s3/private/meta/route.ts
import { s3Service } from '../../_lib';
export const GET = async (req: NextRequest) => {
const documentKey = request.nextUrl.searchParams.get('key') as string;
const metadata = await s3Service.getMetadata(documentKey);
return NextResponse.json(metadata);
};
Client-side (❌ metadata comes back empty)
The problem appears when the same request is made from browser JavaScript.
The call succeeds, but the Metadata object in the AWS SDK response is empty.
// src/app/api/s3/_lib/s3Service.ts
export const getFileMetadata = async (id: string) => {
const response = await s3.send(
new HeadObjectCommand({
Bucket: bucketName,
Key: id,
})
);
return response.Metadata; // {} without proper CORS
};
// src/components/File.tsx
useEffect(() => {
async function setFilename() {
const fileMetadata = await getFileMetadata(file.documentId);
return {
...contract,
// fileMetadata is empty, so this becomes undefined
filename: fileMetadata?.filename,
};
});
}
setFilename();
}, [file]);
The request itself isn’t failing. S3 returns the metadata correctly.
But because the code runs in the browser, certain response headers - including S3’s custom metadata headers - are not exposed to JavaScript by default.
The next section explains why this happens.
Why no Metadata on the Client
Two fundamental security mechanisms are involved here: the Same-Origin Policy (SOP) and Cross-Origin Resource Sharing (CORS).
S3 CORS expose headers, custom metadata missing browser, or AWS CORS not working.
1. Same-Origin Policy
Browsers enforce a rule that pages can only freely access resources from the same “origin,” defined by scheme, host, and port.
Your frontend (https://your-app.com) and S3 (https://your-bucket.s3.amazonaws.com) are different origins, so the browser restricts what your code can access.
2. CORS as the exception
CORS is a system that allows a server to relax these restrictions.
If you configure S3 with an AllowedOrigin matching your frontend, the browser is permitted to make the request.
However, this only covers whether the request can happen - not what the browser exposes back to your JavaScript.
3. Access-Control-Expose-Headers
Even with CORS enabled, the browser only exposes a small set of safe default headers to the client.
S3’s custom metadata headers (x-amz-meta-*) are not part of that list.
This means:
- S3 sends the metadata correctly.
- The browser receives it.
- But the browser filters it out or hides it before the AWS SDK gets the response.
The SDK sees no metadata headers and correctly (from its perspective) returns an empty Metadata object. Unless your S3 bucket explicitly exposes the metadata headers through CORS, your client code will always see Metadata: {}.
How to Fix It with S3 CORS Rules
How do I fix S3 CORS so metadata shows up in HeadObject?
To make your metadata visible in the browser, you need to add your metadata fields to Access-Control-Expose-Headers in your S3 bucket’s CORS config so the browser is allowed to expose the headers you care about.
Add them to its HEAD request responses. This is done by updating the CORS configuration on your S3 bucket, as detailed in the official Amazon S3 User Guide on Managing CORS.
A working XML example looks like this:
<!-- S3 Bucket CORS Configuration (XML) -->
<CORSConfiguration>
<CORSRule>
<AllowedOrigin>https://your-app.com</AllowedOrigin>
<AllowedMethods>GET</AllowedMethod>
<AllowedMethods>HEAD</AllowedMethod>
<AllowedHeaders>*</AllowedHeader>
<ExposeHeaders>x-amz-meta-filename</ExposeHeader>
<ExposeHeaders>x-amz-meta-uploaded-by</ExposeHeader>
<!-- Add an <ExposeHeader> for each custom metadata key you use -->
</CORSRule>
</CORSConfiguration>
JSON Configuration Example:
// S3 Bucket CORS Configuration (JSON)
{
"CORSRules": [
{
"AllowedOrigins": ["<https://your-app.com>"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["x-amz-meta-filename", "x-amz-meta-uploadedat"]
}
]
}
This enables client-side S3 HeadObject to return the full metadata object.
A few important details:
- Expose only the headers you need. The browser will ignore everything else.
- HEAD and GET both matter.
If you’re calling
HeadObjectCommandfrom the client, the HEAD method must be included or the request may fail depending on your setup. - Wildcard
AllowedHeadersis fine here. It simply allows the browser to send custom headers, not expose them.
After this configuration is applied, the browser will no longer filter out the metadata, and your AWS SDK call will return the full Metadata object instead of {}.
👉 Missed Part 1?
Read the full upload/download pipeline here: Stop Uploading Files to S3 the Wrong Way
Top comments (0)