Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for NGINX By James Joyner IV · · 9 min read

NGINX Error Guide: 'upstream sent no valid HTTP/1.0 header' Malformed Backend Response

Fix the NGINX 'upstream sent no valid HTTP/1.0 header' error when your backend returns a malformed or non-HTTP response to the reverse proxy.

  • #nginx
  • #troubleshooting
  • #errors
  • #upstream

Exact Error Message

When NGINX proxies a request to a backend that returns something it cannot parse as an HTTP response, you get a 502 Bad Gateway and a line like this in your error log:

2026/06/27 16:41:18 [error] 2841#2841: *9921 upstream sent no valid HTTP/1.0 header while reading response header from upstream, client: 203.0.113.7, server: app.example.com, request: "GET /api/ HTTP/1.1", upstream: "http://127.0.0.1:9000/api/", host: "app.example.com"

A closely related variant names the offending bytes directly:

2026/06/27 16:42:03 [error] 2841#2841: *9925 upstream sent invalid header: "var_dump(0001a3f4)" while reading response header from upstream, client: 203.0.113.7, server: app.example.com, request: "GET /api/ HTTP/1.1", upstream: "http://127.0.0.1:9000/api/", host: "app.example.com"

In both cases the client receives an HTTP 502. The key phrase is “while reading response header from upstream”: NGINX got some bytes back from the backend, but those bytes were not a well-formed HTTP response header block.

What the Error Means

NGINX, acting as a reverse proxy, opened a connection to your backend (here 127.0.0.1:9000), forwarded the request, and read the reply. Before it can relay that reply to the client, it must parse a valid status line (HTTP/1.0 200 OK or HTTP/1.1 200 OK) followed by header lines, a blank line, then the body.

This error fires when those first bytes are not a valid HTTP header block. NGINX is not complaining about your configuration syntax or about being unable to connect — the connection succeeded and data came back. The problem is on the backend side: the process at the other end of the socket emitted output that is malformed, truncated, prefixed with stray text, or simply not HTTP at all.

Importantly, this is almost never an NGINX bug. NGINX is faithfully reporting that the upstream broke the contract. The fix lives in the backend application or in which port/protocol your proxy_pass points at.

Common Causes

  • Stray output before the headers. A FastCGI/PHP app prints a var_dump(), print_r(), a stack trace, a BOM, or whitespace from an included file before sending headers. Those bytes land where NGINX expects the status line.
  • proxy_pass pointed at a non-HTTP service. The port is listening, but it speaks a different protocol — FastCGI (PHP-FPM), gRPC, a raw TCP/database socket, or a TLS-only endpoint. NGINX reads non-HTTP bytes and gives up.
  • Wrong directive for the protocol. Using proxy_pass http://127.0.0.1:9000 against a PHP-FPM socket that expects fastcgi_pass. PHP-FPM answers in the FastCGI binary protocol, not HTTP.
  • FastCGI app not emitting proper CGI headers. A script that writes a body but never emits Content-Type: or a Status: line, so the gateway cannot build a valid response.
  • Double or malformed Status lines. Two Status: headers, or a Status value NGINX cannot reconcile into a status line.
  • A crashing or garbled backend. The app panics mid-response and writes a partial, binary, or error blob to stdout that NGINX cannot parse.

How to Reproduce the Error

Stand up a tiny “backend” that returns raw, non-HTTP bytes on the port NGINX proxies to:

# Listen on 9000 and answer every connection with plain text, no HTTP headers
while true; do echo "hello, not http" | nc -l -p 9000 -q 1; done

Point a server block at it:

server {
    listen 80;
    server_name app.example.com;
    location /api/ {
        proxy_pass http://127.0.0.1:9000/api/;
    }
}

Reload, then request http://app.example.com/api/. NGINX reads hello, not http, fails to find a status line, returns 502, and logs upstream sent no valid HTTP/1.0 header. A PHP equivalent is adding echo "debug"; at the very top of a script before any header is sent.

Diagnostic Commands

First confirm the config is syntactically valid and see exactly where each upstream points:

# Validate syntax
sudo nginx -t

# Dump the full effective config and find the upstream targets
sudo nginx -T 2>/dev/null | grep -nE 'proxy_pass|fastcgi_pass|server_name'

The decisive step is to curl the backend directly and look at the raw bytes NGINX is choking on:

# -v shows the response status line and headers as curl parses them
curl -sv http://127.0.0.1:9000/api/

# Dump the first 200 raw bytes exactly as the backend sends them
curl -s --output - http://127.0.0.1:9000/api/ | head -c 200; echo

If the first bytes are not HTTP/1.x 200 ... — if you see a var_dump, a PHP warning, HTML, a BOM, or binary garbage — you have found the cause. If curl itself errors with something like “Received HTTP/0.9 when not allowed”, the backend is not speaking HTTP at all.

Confirm what is actually listening on the port and which directive you should be using:

# Which process owns the port, and is it HTTP or FastCGI/other?
sudo ss -ltnp | grep ':9000'

Then read the logs on both sides:

# NGINX errors
sudo journalctl -u nginx --since "10 min ago" --no-pager
sudo tail -n 50 /var/log/nginx/error.log

# Backend service logs (adjust unit name)
sudo journalctl -u php8.2-fpm --since "10 min ago" --no-pager
sudo tail -n 50 /var/log/myapp/app.log

The application log usually shows the warning, notice, or debug statement that is being printed before the headers.

Step-by-Step Resolution

  1. Capture the raw backend bytes. Run the curl -s --output - ... | head -c 200 command above. The leading bytes tell you whether this is stray output, a wrong protocol, or missing CGI headers.

  2. If the bytes are stray output before headers: find the offending statement in the application. Remove the var_dump(), print_r(), debug echo, or print that runs before headers are sent. Check included files for trailing whitespace or a UTF-8 BOM after the closing ?> — these emit bytes too. Ensure the app sends a Content-Type header before any body.

  3. If the port speaks FastCGI, not HTTP: you are using proxy_pass against PHP-FPM. Switch the location to fastcgi_pass 127.0.0.1:9000; with include fastcgi_params; and a proper SCRIPT_FILENAME. PHP-FPM never speaks HTTP, so proxy_pass will always fail against it.

  4. If proxy_pass points at the wrong service entirely: correct the host/port to the real HTTP backend. Verify with ss -ltnp that the target port is the application’s HTTP listener and not a database, gRPC, or TLS-only port.

  5. If the app emits no CGI headers: make the script output a valid header block — at minimum a Status: and Content-Type: line followed by a blank line before the body. Remove any duplicate Status: lines.

  6. Re-test the backend in isolation with curl -sv until the first line reads HTTP/1.1 200 OK and headers look correct.

  7. Reload NGINX and verify end to end:

sudo nginx -t && sudo systemctl reload nginx

Then request the URL through NGINX and confirm a 200 with no new error.log entries.

Prevention and Best Practices

  • Disable debug output in production. Turn off PHP display_errors, route warnings to logs, and strip var_dump/print_r from request paths before deploying.
  • Match the directive to the protocol. Use fastcgi_pass for PHP-FPM, proxy_pass for HTTP backends, and grpc_pass for gRPC. Never cross them.
  • Health-check the backend directly in CI or monitoring with curl -sv so a malformed first line is caught before NGINX users see a 502.
  • Avoid closing ?> tags in pure-PHP files and verify files are saved without a BOM to prevent stray bytes.
  • Pin the upstream port in ss checks during deploys so you never proxy to a port that has been reassigned to a different service.
  • upstream sent invalid header — the same root cause; NGINX shows you the exact bad header bytes.
  • upstream prematurely closed connection while reading response header — the backend crashed or closed the socket before any headers; check the app logs for a panic.
  • 502 Bad Gateway — the generic client-facing symptom this error produces; the error.log line tells you which flavor.
  • upstream sent too big header — valid HTTP, but headers exceed proxy_buffer_size; a different, buffer-tuning fix.

See more in the NGINX category.

Frequently Asked Questions

Is this an NGINX bug or a misconfiguration on my side?

Neither, usually. NGINX is correctly reporting that the backend returned bytes that are not a valid HTTP response. The fix is on the backend: either it emits stray output before its headers, or your proxy_pass/fastcgi_pass points at something that does not speak HTTP. NGINX is the messenger, not the cause.

Why does curl from my shell work but NGINX still 502s?

Check what you curled. If you curled the public NGINX URL it may be cached or you hit a different path. Curl the backend directly (curl -sv http://127.0.0.1:9000/api/) on the exact path from the error log. That reproduces what NGINX sees and exposes the malformed first line.

I am proxying to PHP-FPM. Why is this happening?

PHP-FPM speaks the FastCGI binary protocol, not HTTP. If you used proxy_pass, NGINX reads FastCGI bytes as if they were HTTP and fails. Switch to fastcgi_pass with include fastcgi_params;. If you already use fastcgi_pass, the cause is your script printing output before its headers.

How do I find the exact stray bytes?

Run curl -s --output - http://127.0.0.1:9000/api/ | head -c 200. The leading characters — a var_dump, a PHP warning, an HTML fragment, or a BOM — are precisely what NGINX rejected. Grep your code for that string to locate the line emitting it.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.