Fixing Python requests That Returns 200 But Response Body Is Empty

June 20, 2026 8 min read 3 views

You call an API, the server responds with 200 OK, and your code crashes because response.text is an empty string. The status code is supposed to mean "everything worked," so an empty body feels like a contradiction. It isn't — there are at least six distinct reasons this happens, and each one has a straightforward fix.

What You'll Learn

  • How to confirm whether the body is truly empty or just misread
  • How encoding, compression, and redirects silently strip body content
  • How missing request headers cause servers to return empty responses
  • How to handle streamed and chunked responses correctly
  • How to distinguish a real empty body from an authentication failure in disguise

Prerequisites

You need Python 3.8 or later and the requests library installed (pip install requests). The examples below also use the standard-library json module. No other dependencies are required.

Why a 200 Response Can Still Be Empty

HTTP 200 only means the server processed your request without a protocol-level error. It says nothing about whether the body contains what you expected. The body can be empty for reasons on either side of the connection: the server chose not to send content, the content was sent but encoded in a way your code didn't decode, or a redirect along the way dropped the original response body entirely.

Before you touch your code, capture the raw response details so you know which category you're in:

import requests

r = requests.get("https://example.com/api/data")

print("Status:", r.status_code)
print("Headers:", dict(r.headers))
print("Encoding:", r.encoding)
print("Content-Length:", r.headers.get("Content-Length"))
print("Content bytes:", len(r.content))
print("Text preview:", repr(r.text[:200]))

Run this first. The output will usually point you directly at the culprit.

Check response.content Before Assuming Nothing Came Back

response.text is response.content decoded to a string using response.encoding. If the encoding is wrong, the decoding can silently produce an empty string or a string full of replacement characters rather than raising an error.

Always check response.content (raw bytes) first:

import requests

r = requests.get("https://example.com/api/data")

if r.content:
    print("Bytes received:", len(r.content))
    # Decode manually if the auto-detected encoding is wrong
    text = r.content.decode("utf-8", errors="replace")
    print(text[:500])
else:
    print("Body is genuinely empty at the byte level")

If r.content has bytes but r.text is empty or garbled, you have an encoding mismatch. Force the correct encoding before calling r.text:

r.encoding = "utf-8"  # or whatever the server actually uses
print(r.text)

The Content-Type response header usually tells you the intended charset: Content-Type: application/json; charset=utf-8. When the header is missing or wrong, requests falls back to ISO-8859-1 for text content types, which will misread multibyte characters.

The Server Sent Gzip or Brotli and Requests Didn't Decode It

requests automatically decompresses gzip responses, but only when the Content-Encoding: gzip header is present. Some servers compress the body without setting that header correctly, or they use Brotli (br) which requests does not support natively.

Check what the server declared:

print(r.headers.get("Content-Encoding"))  # e.g. "gzip", "br", "deflate"

If the encoding is br (Brotli), install the optional dependency and let requests handle it:

pip install brotli

With brotli installed, requests will decompress Brotli responses automatically as long as the Content-Encoding: br header is present. If the server sends compressed bytes without the header, you have to decompress manually:

import gzip
import requests

r = requests.get("https://example.com/api/data")

try:
    body = gzip.decompress(r.content).decode("utf-8")
except OSError:
    body = r.text  # wasn't gzip after all

print(body)

A Redirect Swallowed the Body

By default, requests follows redirects automatically. When a server redirects a POST request, most implementations convert it to a GET on the new URL. If the endpoint on the other end only produces content for POST, the redirected GET returns an empty body with a 200 status.

Disable redirects temporarily to see what's happening:

import requests

r = requests.get("https://example.com/api/data", allow_redirects=False)
print(r.status_code)         # might be 301, 302, 307, etc.
print(r.headers.get("Location"))  # the URL it wanted to send you to

If you see a 3xx status and a Location header, the real content lives somewhere else. You can inspect each step of the redirect chain using r.history:

r = requests.get("https://example.com/api/data")
for step in r.history:
    print(step.status_code, step.url)
print("Final:", r.status_code, r.url)

If the redirect chain converts your method, switch to requests.post() with allow_redirects=True and confirm the final URL accepts POST directly.

The Server Expects Specific Headers

Some APIs are picky. They return an empty body (still with a 200) when the Accept header doesn't match a content type they can produce, or when the User-Agent looks like a bot they want to suppress. This is more common than you'd expect with scraping-sensitive endpoints and older enterprise APIs.

Add the headers the server expects:

import requests

headers = {
    "Accept": "application/json",
    "Accept-Encoding": "gzip, deflate",
    "User-Agent": "MyApp/1.0 (internal; contact@example.com)",
    "Accept-Language": "en-US,en;q=0.9",
}

r = requests.get("https://example.com/api/data", headers=headers)
print(r.text)

To identify exactly which header is required, strip them back one by one. Start with all the headers a browser would send, then remove them until the response breaks. That isolates the required one.

If you're working with token-based authentication and hitting similar issues in other parts of your stack, the debugging pattern in fixing Python requests that hang indefinitely without raising a timeout error also covers how to instrument request headers during troubleshooting.

The Response Is Chunked or Streamed

When a server sends a chunked Transfer-Encoding, the body arrives in pieces. Most of the time requests assembles them transparently. But if you open the response in streaming mode and then read r.text without consuming the stream first, you may get nothing.

Streaming mode requires you to explicitly read the content:

import requests

with requests.get("https://example.com/api/stream", stream=True) as r:
    r.raise_for_status()
    chunks = []
    for chunk in r.iter_content(chunk_size=8192):
        if chunk:  # filter out keep-alive empty chunks
            chunks.append(chunk)
    body = b"".join(chunks).decode("utf-8")

print(body)

If you're not intentionally using stream=True, remove it. In non-streaming mode, requests reads the full body before returning the response object, so r.text will always have the complete content.

For newline-delimited JSON streams (common in LLM APIs and event feeds), iterate line by line instead:

import requests
import json

with requests.get("https://example.com/api/ndjson", stream=True) as r:
    for line in r.iter_lines():
        if line:
            record = json.loads(line.decode("utf-8"))
            print(record)

The Endpoint Returns 204 or the Body Is Genuinely Empty

Some endpoints return 200 with an intentionally empty body as a "success, nothing to report" signal. Others are misconfigured and should be returning 204 No Content but send 200 instead. Both look the same from your side.

Check Content-Length:

content_length = r.headers.get("Content-Length")
print("Content-Length:", content_length)  # "0" means the server declared an empty body
print("Actual bytes:", len(r.content))    # double-check at the byte level

If Content-Length is 0 and len(r.content) is also 0, the server genuinely sent nothing. At that point the question is whether the API documentation says it should. If the docs say you should receive JSON, something is wrong on the server side or you are hitting the wrong endpoint — possibly a staging URL or a version mismatch.

Session State and Authentication Failures That Look Like 200

A surprisingly common case: the server returns 200 but the body is actually a login redirect page or an "Unauthenticated" JSON payload that your code fails to parse. When you call response.json() on HTML, it raises a JSONDecodeError. When you check response.text expecting a JSON string, you get HTML that looks empty if you only check length after stripping tags.

Print the raw body unconditionally during debugging:

import requests

r = requests.get(
    "https://example.com/api/protected",
    headers={"Authorization": "Bearer YOUR_TOKEN"},
)

print("Content-Type:", r.headers.get("Content-Type"))
print("Body (first 500 chars):", r.text[:500])

If the Content-Type says text/html when you expected application/json, you're reading an error page or a login redirect. Fix the authentication, not the response parsing.

When using session-based auth with cookies, use a requests.Session object so cookies persist across calls:

import requests

session = requests.Session()

# Authenticate first
session.post("https://example.com/login", json={"username": "u", "password": "p"})

# Now cookies are attached automatically
r = session.get("https://example.com/api/protected")
print(r.text)

Common Pitfalls When Debugging Empty Responses

A few patterns trip people up repeatedly when chasing this bug.

Calling response.json() before checking content type. If the body isn't JSON, the exception message is misleading. Always check r.headers["Content-Type"] and r.text before calling r.json().

Assuming the URL is correct. A typo in the path or a missing trailing slash can silently land you on a different route that returns an empty body. Log r.url (not just the URL you passed in) to see the final URL after redirects.

Ignoring response headers. The headers carry most of the diagnostic information: Content-Type, Content-Encoding, Content-Length, Transfer-Encoding. Print them all as a dict at the start of any debugging session.

Reusing a response object after streaming. Once you've consumed a stream, the body is gone. You cannot read r.text after iterating r.iter_content() on the same object. Store the assembled bytes in a variable first.

Not raising on HTTP errors. Add r.raise_for_status() early in your code. Some servers return 200 on error paths too, but more often than not you're actually getting a 401 or 403 that requests followed through a redirect to a 200 error page. Raising on status forces you to see those cases clearly. The same principle applies to other Python I/O bugs — being explicit about what you expect is always the faster path to a fix, as covered in articles like fixing Python sqlite3 that returns stale data after a commit and fixing Python datetime.strptime that raises ValueError on valid date strings.

Wrapping Up

An empty body on a 200 response usually comes down to one of these six causes: encoding mismatch, missing decompression, redirect method change, missing request headers, unread stream, or a disguised authentication failure. Here are the concrete steps to resolve it:

  1. Print r.status_code, dict(r.headers), len(r.content), and repr(r.text[:200]) at the point of failure — this alone resolves most cases.
  2. Check r.content first. If bytes are present but r.text is empty, force the encoding with r.encoding = "utf-8".
  3. Disable redirects with allow_redirects=False and inspect r.history to see if a redirect is dropping the body.
  4. Add explicit Accept, User-Agent, and Authorization headers, then strip them back one by one to find which one the server requires.
  5. If using stream=True, consume the body with iter_content() or iter_lines() and store the result before accessing it.

Frequently Asked Questions

Why does Python requests return status 200 but response.text is an empty string?

A 200 status only means the server accepted the request — the body can still be empty due to an encoding mismatch, incorrect decompression, a redirect that changed the HTTP method, or a missing header the server requires. Check response.content for raw bytes first; if bytes are present but response.text is empty, you have an encoding issue rather than a missing body.

How do I check if a requests response body is truly empty versus being misread?

Compare response.content (raw bytes) against response.text. If len(response.content) is greater than zero but response.text is empty or garbled, the body was received but decoded incorrectly. Set response.encoding to the correct charset (usually 'utf-8') before accessing response.text.

Can HTTP redirects cause an empty response body in Python requests?

Yes. When requests follows a redirect, it can change a POST to a GET, and the new endpoint may return nothing for a GET request. Use allow_redirects=False and inspect r.history to see whether a redirect is happening and which method is used on each hop.

Why does requests return empty content when I use stream=True?

In streaming mode, the body is not read automatically — you must consume it using iter_content() or iter_lines(). If you access r.text without consuming the stream first, you get an empty string. Collect all chunks into a variable and decode them before using the result.

How do I debug a Python requests call that gets 200 OK but no JSON data?

Print the full response headers and the first 500 characters of response.text unconditionally. Check that Content-Type is application/json rather than text/html, which would indicate you are receiving an error page or login redirect instead of the expected API data. Then verify your Authorization header or session cookies are being sent correctly.

📤 Share this article

Sign in to save

Comments (0)

No comments yet. Be the first!

Leave a Comment

Sign in to comment with your profile.

📬 Weekly Newsletter

Stay ahead of the curve

Get the best programming tutorials, data analytics tips, and tool reviews delivered to your inbox every week.

No spam. Unsubscribe anytime.