While updating the code to match a new API server domain after it had changed, I ran into an HTTP 426 Upgrade Required error. As I worked through the fix, I realized this was an error I had rarely encountered, so I want to write down a quick summary of it.
The HTTP 426 Upgrade Required described on MDN is defined as "the server refuses to perform the request using the current protocol but might be willing to do so after the client upgrades to a different protocol."
The server usually indicates the required protocol by including it in the Upgrade response header.
The API communication flow of the affected service is as follows.
- The client calls the API through the web server domain
- The web server, built on nginx, proxies the request to the API server domain (proxy_pass)
- The client receives an HTTP 426 Upgrade Required response
The problem was that no logs appeared on either the API server or Nginx, and that proxying to the local Vite Dev Server worked without any issue.

Nginx proxy_pass
After confirming that it worked correctly with the Vite Dev Server, I suspected that the problem might be in the reverse proxy process where Nginx receives the client request and forwards it to the API server. I ran tests while changing various settings such as host and resolver. During the process of configuring the http protocol, I confirmed that it worked correctly with settings of 1.1 or higher.
Afterward, I found in the official Nginx proxy_pass documentation that the default behavior is 1.0, and I was able to solve the problem by setting proxy_http_version.
why between nginx/nginx upstream use http/1.0?
From its initial design, Nginx was built to be used as a lightweight web server + proxy server. At the time of that design, in the early-to-mid 2000s, many backend servers and CGI frameworks did not fully support HTTP/1.1. So Nginx set HTTP/1.0 as the default for connections to the backend, in order to work well with as many systems as possible.
proxy_pass
While solving the problem above, I didn't fully understand the proxy_pass configuration, so I experimented with various settings while testing. Since I was at it, I want to also put together a brief summary of proxy_pass.
As the directive that implements a reverse proxy, proxy_pass is the setting by which nginx forwards a request it received to another server. In practice, it forwards the request received from the client to another server, receives the response, and sends it back to the client. Here, the client sees nginx as the request/response server, but the response is actually handled by a different server.
To configure it, you set the path to be proxied and then set the server URL to which the request will be forwarded via proxy_pass.
jsonlocation /api {
**proxy_pass** https://api-server;
}
A point to be careful about when setting the server URL is that the presence or absence of a trailing / changes how the API proxy path is handled.
Without a /, the path configured in location is appended as-is and forwarded; with a /, it is omitted before being forwarded to the server.
GET /api/user → https://api-server/api/user
jsonlocation /api {
**proxy_pass** https://api-server;
}
GET /api/user → https://api-server/user
jsonlocation /api {
**proxy_pass** https://api-server/;
}
options
I'll also briefly summarize the header settings that can affect API request/response during proxying, along with the HTTP version setting I configured to resolve the HTTP 426 Upgrade Required problem.
| Setting | Description |
|---|---|
proxy_set_header Host $host; | Forward the Host header as in the original request |
proxy_set_header X-Real-IP $remote_addr; | Pass the client IP to the backend |
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | Pass the proxy hop history |
proxy_http_version 1.1; | Force the HTTP version (required for WebSocket, etc.) |
proxy_read_timeout 60s; | Set the wait time for the backend response |
In conclusion
Resolving the HTTP 426 Upgrade Required error became a good opportunity to revisit the service's API request-to-response flow and the Nginx reverse proxy from the start. And while HTTP/1.1 became the standard back in 1997, and there was a historical context for Nginx setting HTTP/1.0 as its default, it still struck me as a very conservative design choice.
Also, while gathering material and putting this together, I heard from GPT that Nginx's design philosophy is explicit is better than implicit, and it made me curious about the philosophies of other languages.
Below is an article that criticizes Python and Rust for touting their explicitness as an advantage. After reading it, I think I should discuss with the colleagues I work with what they think about explicitness.
