S3 GetObject: Read the Body as Text, JSON or a File in AWS SDK v3, boto3, CLI and PowerShell (Fix [object Object] and Stream Errors)

Logeshwaran
—

To read a file from Amazon S3 in code, you call GetObject and then read its Body, which is a stream, not text. In the AWS SDK for JavaScript v3: const res = await s3.send(new GetObjectCommand({ Bucket, Key })), then await res.Body.transformToString() for text (or JSON.parse of it), transformToByteArray() for bytes, or pipeline(res.Body, fs.createWriteStream(path)) to save a large file without loading it into memory. Calling res.Body.toString() gives [object Object], the most common v2-to-v3 surprise, because v2 returned a Buffer. In Python boto3: s3.get_object(Bucket=..., Key=...)["Body"].read().decode("utf-8"), or s3.download_file() for files. In the AWS CLI: aws s3 cp s3://bucket/key - to print it, or aws s3api get-object --bucket B --key K out.txt. In PowerShell, Read-S3Object downloads; Get-S3Object only lists. Read the body only once, and always read or discard it, or your app runs out of sockets. Below: working code for each language, JSON, ranges, presigned URLs, TypeScript types, browser use, permissions, and every error.

Jake's first Lambda function had one job: read a price list stored as JSON in S3 and return it. It ran, and returned [object Object]. He had copied the code from an old tutorial written for the previous version of the JavaScript SDK, where the file arrived as a ready-made buffer. In version 3 it arrives as a stream, a pipe the data flows through, and calling toString() on the pipe describes the pipe, not the water. One method call fixed it.

⚡ Quick Answer

• JavaScript v3: await res.Body.transformToString(). Code.

• Python: obj["Body"].read().decode("utf-8"). Code.

• CLI: aws s3 cp s3://bucket/key -. CLI and PowerShell.

• Big files: stream to disk, never load into memory. Streaming.

Why it's a stream · TypeScript · Browser · Ranges · Errors.

New to S3 or the JavaScript SDK? The cards below cover S3, missing keys, credentials and SDK v3 in ES modules.

🧭 NEW HERE? READ THESE FIRST

New to S3 or the AWS SDKs? These five pages give you the background this one builds on:

📌 Bookmark this; the per-language code and the error list are the parts you will come back for.

Why the Body is a stream, not text

An S3 object can be a 2 KB JSON file or a 50 TB video. If every SDK loaded the whole object into memory before handing it to you, large files would crash your program. So GetObject returns the object's details (size, type, ETag, metadata) at once, and the content as a stream you read when you are ready: all at once for small files, chunk by chunk for big ones. Our plain-English S3 guide covers objects and keys if those are new.

Two rules follow from that, in every language:

  • You can read the body once. After you read it to the end, it is empty. Keep the result in a variable if you need it twice.
  • You must read it, or discard it. An unread body keeps its network connection open. Do that in a loop and your app runs out of connections and starts waiting or timing out.

Jake: "So the SDK hands me a tap, not a glass of water?"

Ethan: "Right. Turn it on, fill the glass, and don't leave taps running."

The same job in each tool you are likely to use:

ToolRead as textSave to a fileProcess big files
JavaScript SDK v3await Body.transformToString()pipeline(Body, createWriteStream(p))Pipe through readline or zlib
Python boto3Body.read().decode("utf-8")s3.download_file(b, k, p)Body.iter_lines() / iter_chunks()
Java SDK v2getObjectAsBytes(req).asUtf8String()ResponseTransformer.toFile(path)getObject(req) as an InputStream
AWS CLIaws s3 cp s3://b/k -aws s3 cp s3://b/k ./filePipe aws s3 cp ... - into another command
PowerShellRead-S3Object then Get-ContentRead-S3Object -File pathDownload, then read line by line

JavaScript (AWS SDK v3): text, JSON, bytes, file

import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";

const s3 = new S3Client({ region: "us-east-1" });
const res = await s3.send(new GetObjectCommand({ Bucket: "shop-data", Key: "prices.json" }));

// Text
const text = await res.Body.transformToString();          // UTF-8 by default
// JSON
const prices = JSON.parse(text);
// Bytes (Uint8Array), for images, PDFs, zip files
// const bytes = await res.Body.transformToByteArray();
// Save to a file without loading it into memory (use instead of the lines above)
// await pipeline(res.Body, createWriteStream("/tmp/prices.json"));

console.log(res.ContentType, res.ContentLength, prices.length);

Pick one of the three ways to read res.Body; each consumes it. transformToString("latin1") takes another encoding if your file is not UTF-8. The response also carries ContentType, ContentLength, ETag, LastModified and Metadata (your custom x-amz-meta-* values) without reading the body.

Coming from SDK v2? The old code looked like this:

// SDK v2 (old): Body was a Buffer, so toString() worked
const data = await s3.getObject({ Bucket, Key }).promise();
const text = data.Body.toString("utf-8");

In v3, Body is a stream with the transformTo... helpers, there is no .promise(), and you send command objects. Moving your code over, step by step:

  1. Install the client you need: npm install @aws-sdk/client-s3 (each service is its own small package).
  2. Create one client per Region and reuse it: new S3Client({ region }), outside your Lambda handler so it is reused between calls.
  3. Replace each call s3.getObject(params).promise() with s3.send(new GetObjectCommand(params)).
  4. Replace each body read data.Body.toString() with await res.Body.transformToString().
  5. Update error checks: v3 errors carry err.name (such as "NoSuchKey") and typed classes you can test with instanceof, instead of v2's err.code.

Step 4 alone fixes the most common migration bug, the [object Object] one. In Lambda's Node.js runtimes, v3 is the SDK that is included; our guide to SDK v3 in ES modules covers the import side.

TypeScript: the Body type

In TypeScript, res.Body is typed for both Node.js and browsers, so passing it to Node stream functions needs a hint:

import type { Readable } from "node:stream";

const res = await s3.send(new GetObjectCommand({ Bucket, Key }));
if (!res.Body) throw new Error("empty body");

const text: string = await res.Body.transformToString();     // no cast needed
// For Node stream APIs:
await pipeline(res.Body as Readable, createWriteStream(path));

The transformTo... methods need no cast; only Node-specific stream functions do. The if (!res.Body) check is worth keeping: the type allows undefined, and an explicit check gives you a clear error instead of a confusing one later. If your project targets both Node and the browser, keep the S3 reading code in one module and use only the transformTo... helpers there, so it works in both places unchanged.

Jake: "So TypeScript was right to complain?"

Ethan: "It was telling you the same body works in two very different places. You just tell it which one you're in."

Python (boto3): read, decode, stream, download

import json
import boto3

s3 = boto3.client("s3")
obj = s3.get_object(Bucket="shop-data", Key="prices.json")

# Text and JSON (small files)
text = obj["Body"].read().decode("utf-8")
prices = json.loads(text)

# Line by line, for big text files such as CSV or logs
obj = s3.get_object(Bucket="shop-data", Key="logs/2026-10-19.csv")
for line in obj["Body"].iter_lines():
    row = line.decode("utf-8").split(",")

# Straight to a file (handles large objects in parallel parts)
s3.download_file("shop-data", "backups/db-backup.sql.gz", "/tmp/db-backup.sql.gz")

# Into memory as a file-like object
import io
buf = io.BytesIO()
s3.download_fileobj("shop-data", "images/logo.png", buf)

Your obj["Body"] is a StreamingBody, the boto3 version of the same tap: read() once, or iterate with iter_lines() or iter_chunks(). For anything bigger than a few hundred MB, download_file is the better choice: it splits the download into parallel ranges and retries parts that fail. Our S3 listing guide shows how to loop over many objects first.

AWS CLI and PowerShell

# Print an object to the terminal (the "-" means standard output)
aws s3 cp s3://shop-data/prices.json -

# Download to a file
aws s3 cp s3://shop-data/prices.json ./prices.json

# Low-level call: the last argument is the file to write (required)
aws s3api get-object --bucket shop-data --key prices.json prices.json

# Only part of a file
aws s3api get-object --bucket shop-data --key big.log --range bytes=0-1023 head.log

aws s3 cp is the high-level command you will use most: it handles large files in parallel parts and shows progress. aws s3api get-object maps one-to-one to the API call, which is useful when you need options such as --range, --version-id or --if-none-match, and it always needs an output file name as its last argument. Printing with - lets you chain commands, for example aws s3 cp s3://shop-data/prices.json - | jq 'first' to look at the first price without saving anything.

In AWS Tools for PowerShell, the names trip people up:

# Download an object to a file
Read-S3Object -BucketName shop-data -Key prices.json -File C:\temp\prices.json

# Read it as text
Get-Content C:\temp\prices.json -Raw | ConvertFrom-Json

# Get-S3Object does NOT return content: it lists objects (keys, sizes, dates)
Get-S3Object -BucketName shop-data -KeyPrefix invoices/

Large files: stream, don't buffer

Reading a whole object into a string or byte array is fine for kilobytes and small megabytes. For larger files it can exhaust your memory, especially in Lambda, where memory is what you set for the function. The safe patterns:

  1. Stream straight to disk: pipeline(res.Body, createWriteStream(path)) in Node, download_file in Python. In Lambda, /tmp holds up to the ephemeral storage you configure.
  2. Process as it flows: pipe the body through a line reader or a decompressor (zlib.createGunzip()) and handle each line or chunk, so memory stays flat whatever the file size.
  3. Read only what you need with a byte range (next section), for example the header of a big CSV.
  4. Use the transfer helpers for very large objects: they download in parallel parts and retry failed parts for you.
  5. Skip the download when you only need a few rows: query the file in place with Athena or S3 Select-style tools instead of pulling it all into your code.

Byte ranges and conditional reads

// First 1 KB only
const head = await s3.send(new GetObjectCommand({ Bucket, Key, Range: "bytes=0-1023" }));

// Only if it changed since you last read it (saves bandwidth)
const fresh = await s3.send(new GetObjectCommand({ Bucket, Key, IfNoneMatch: lastEtag }));
// Unchanged objects return a 304 Not Modified error you can catch

Ranges are how download managers and video players read big files in pieces, and how you peek at a file's start cheaply. The response to a ranged request has status 206 and a ContentRange header telling you which bytes you got and the full size, so you can work out the next range yourself.

Jake: "So I could read just the CSV header to check the columns?"

Ethan: "First kilobyte, split on the first newline. No need to download the other two gigabytes."

Each ranged GET is billed as one request. To check whether an object exists, or to read its size and metadata without the body at all, use HeadObjectCommand (head_object in boto3).

In the browser, and presigned URLs

In a browser, res.Body is a web ReadableStream, not a Node stream, so Node-only calls such as .pipe() fail with "body.pipe is not a function". The transformToString() and transformToByteArray() helpers work the same, and transformToWebStream() gives you the web stream directly.

For most web apps, though, the browser should not hold AWS credentials at all. Generate a presigned URL on your server and let the browser fetch the file directly:

import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const url = await getSignedUrl(s3, new GetObjectCommand({ Bucket, Key }), { expiresIn: 900 });
// send url to the browser; it can GET the file for 15 minutes

When the browser reads the file with fetch from another site's page, the bucket needs a CORS rule; our S3 CORS guide covers it, and our presigned URL guide explains expiry errors.

Permissions GetObject needs

  • s3:GetObject on the object ARN (arn:aws:s3:::shop-data/* or a narrower prefix).
  • s3:ListBucket on the bucket ARN, if you want a missing key to return NoSuchKey (404) instead of AccessDenied (403).
  • kms:Decrypt on the key, if the object is encrypted with SSE-KMS.
  • s3:GetObjectVersion if you read a specific VersionId.

For a Lambda function or an EC2 instance, these go on the function's execution role or the instance profile, never in access keys inside your code. Keep the resource as narrow as your app allows, for example arn:aws:s3:::shop-data/prices/*, so a bug in your code cannot read the rest of the bucket. Every AccessDenied variant on GetObject, including bucket policies, KMS and VPC endpoints, is in our GetObject AccessDenied guide.

GetObject errors, fixed

What you seeCauseFix
[object Object] instead of the file's textSDK v3 Body is a stream; toString() describes the streamawait res.Body.transformToString()
.promise is not a functionv2 code running against v3Use s3.send(new GetObjectCommand(...))
Body empty the second time you read itStreams can be read only onceStore the first result in a variable
body.pipe is not a functionBrowser web stream, not a Node streamUse the transformTo... helpers
Warnings that socket usage is at capacity; requests slow or time outBodies left unread keep connections openAlways read or destroy each Body
NoSuchKey (404)Wrong key, case, prefix or RegionCheck the exact key; see our NoSuchKey guide
AccessDenied (403)Missing s3:GetObject, KMS, or a bucket policy deny (or a missing key without ListBucket)Fix the policy; add ListBucket to see 404s
Lambda runs out of memory on a big fileWhole object loaded into a string or bufferStream to /tmp or process chunks
Garbled charactersFile is not UTF-8Pass the right encoding to transformToString or decode()
Gzip bytes instead of textObject stored compressedDecompress with zlib / gzip after reading

Missing-key problems have their own checklist in our NoSuchKey guide, and "could not load credentials" before the call even starts is covered in our SDK credentials guide.

Frequently asked questions

How do I read an S3 object as a string in AWS SDK v3?

Send GetObjectCommand, then call await res.Body.transformToString().

Why does Body.toString() return [object Object] in SDK v3?

In v3 the Body is a stream, not a Buffer. Use transformToString() or transformToByteArray().

How do I read a JSON file from S3 in JavaScript?

JSON.parse(await res.Body.transformToString()).

How do I save an S3 object to a file in Node.js?

await pipeline(res.Body, createWriteStream(path)) with pipeline from node:stream/promises.

How do I read an S3 object in Python boto3?

s3.get_object(Bucket=..., Key=...)["Body"].read().decode("utf-8"), or s3.download_file() for files.

How do I read a large S3 file line by line in Python?

Iterate over obj["Body"].iter_lines(), decoding each line.

How do I print an S3 object with the AWS CLI?

aws s3 cp s3://bucket/key - writes it to standard output.

What is the difference between Get-S3Object and Read-S3Object?

Get-S3Object lists objects; Read-S3Object downloads an object's content to a file.

Can I read the S3 Body twice?

No. A stream can be read once; keep the result in a variable.

How do I read only part of an S3 object?

Pass Range: "bytes=0-1023" to GetObject, or --range in the CLI.

How do I check if an S3 object exists without downloading it?

Use HeadObject: it returns size and metadata, or a 404 if the key does not exist.

What permissions does GetObject need?

s3:GetObject on the object, kms:Decrypt for SSE-KMS objects, and s3:ListBucket if you want 404s for missing keys.

How do I let a browser download a private S3 file?

Create a presigned URL on your server with getSignedUrl and give it to the browser.

What is the TypeScript type of the S3 Body?

A union for Node and browsers; use transformToString without a cast, or cast to Readable for Node stream APIs.

Why do my S3 requests slow down after many GetObject calls?

Unread bodies keep sockets open; read or destroy every Body so connections return to the pool.

How do I migrate getObject from SDK v2 to v3?

Replace s3.getObject(params).promise() with s3.send(new GetObjectCommand(params)) and Body.toString() with await Body.transformToString().

How do I download a whole S3 object in Java?

With SDK for Java v2, s3.getObjectAsBytes(request).asUtf8String() for text, or getObject with ResponseTransformer.toFile(path).

For readers preparing for an AWS certification

We have been covering AWS certifications alongside our fixes, so here is a short extra for readers studying for one. If you only came for the fix, you already have it; feel free to skip this part.

Exams that test this topic: Developer – Associate (DVA-C03 from December 1, 2026) • Solutions Architect – Associate (SAA-C03) • CloudOps Engineer – Associate (SOA-C03).

Here are the exam-style questions this topic tends to raise, with the reasoning you need to answer them:

1. A web app must let browsers download private S3 objects without AWS credentials. What should the server provide?

A presigned URL for GetObject with a short expiry.

2. Which S3 API checks whether an object exists and returns its metadata without the content?

HeadObject.

3. Which IAM permission lets a Lambda function read objects from a bucket?

s3:GetObject on the object ARN (plus kms:Decrypt for SSE-KMS objects).

4. A function must read only the first kilobyte of a large S3 object. How?

A GetObject request with a Range header such as bytes=0-1023.

5. Why should a missing key return 404 instead of 403 for your application, and what enables it?

So code can tell "not found" from "denied"; grant s3:ListBucket on the bucket.

6. How should an application on EC2 get credentials to read S3?

From an IAM role attached to the instance, not from access keys in code.

7. A Lambda function runs out of memory reading a 2 GB S3 object. What is the better approach?

Stream the object to /tmp or process it in chunks instead of loading it into memory.

Exam codes and dates change often. Our AWS certification changes page keeps track of what is current.

πŸ“š ALSO READ

Where to go next, from AccessDenied to presigned URLs:

📌 Bookmark this; the per-language code and the error list are the parts you will come back for.

Jake's Lambda now returns the price list in 40 milliseconds: GetObjectCommand, transformToString, JSON.parse, done. When the shop later added a 2 GB nightly export, he did not reuse that code; he streamed it to /tmp with pipeline, and the function's memory graph stayed flat. Ethan's one-line review comment on the pull request: "Tap, not glass. Good."

📌 If you keep one line from this page

GetObject's Body is a stream: in SDK v3 read it with transformToString or pipe it to a file, in boto3 call read() or download_file, read it only once, and never leave a body unread.

toString() = [object Object] · Range for partial reads · HeadObject to check existence · presigned URLs for browsers.

Revision note. Written October 19, 2026. An S3 Body is a tap you open once; every error on this page comes from treating it as a glass.

Related