Redirects, Content Negotiation and Proxies
By Syncfusion®, Inc. · Last updated
Content negotiation with Accept headers, req.accepts and res.format, 406, redirect status codes, res.redirect and res.location, building and checking redirect URLs, open redirects, reverse proxies, trust proxy and forwarded headers.
What you will learn
- Answer in the format and language a client asks for
- Choose the right redirect status
- Build safe redirect URLs
- Prevent open redirects
- Configure trust proxy for a reverse proxy
Response Control Introduction
This lesson is about sending the right answer in the right way.
- Content negotiation = picking the answer's format from what the client asks for.
- Redirect = telling the client "what you want is at another URL".
- Reverse proxy = a server in front of Express that passes requests on.
- Proxies change what Express sees, unless you tell Express to trust them.
- All three use special headers:
Accept,LocationandX-Forwarded-*.
Simple lesson: Answer in the format asked for, point to the right place, and know who is really asking.
A Real-World Example
A school notice board serves many readers.
- A parent asks for the notice in Tamil. A teacher asks for it in English. Same notice, different language.
- The canteen moved to Block B. A sign on the old door says "Now in Block B".
- Visitors sign in at the gate. The guard writes "Visitor from Gate 2" before sending them in.
- Staff believe the guard's note, but not a note a visitor wrote themselves.
The language choice is negotiation. The sign is a redirect. The trusted guard is the proxy.
In this lesson, you will learn:
- How clients ask for formats, languages and encodings.
- How to answer with
res.formatandreq.accepts. - Redirect status codes and
res.redirect. - How to build safe redirect URLs.
- How reverse proxies and
trust proxywork. - How to stay safe with forwarded headers.
Content Negotiation
The client says what it prefers in request headers:
- Accept: formats, such as
application/jsonortext/html. - Accept-Language: languages, such as
ta-IN, en;q=0.8. - Accept-Charset: character sets. Almost always UTF-8 today.
- Accept-Encoding: compression, such as
gzip, br. - q = a preference from 0 to 1. Higher is preferred.
Express reads them with matching methods:
- req.accepts(types) returns the best format you offer, or
false. - req.acceptsLanguages(langs), req.acceptsCharsets(...) and req.acceptsEncodings(...) work the same way.
- French is not offered, so the code falls back to English.
Varytells caches that the answer depends onAccept-Language.
Simple lesson: Read the client's preference, and always have a default.
Answering with res.format
res.format(handlers) = run the handler that matches the client's Accept header.
- Keys are formats:
html,json,text, or full types likeapplication/json. - Express sets
Content-TypeandVary: Acceptfor you. - default runs when nothing matches.
- Without
default, Express answers 406 Not Acceptable. - One route can serve a web page and an API.
image/pngmatches nothing, so Express answers 406.- Add
default: () => res.json({ items })to answer JSON instead.
Easy way to remember: 406 means "I can't speak that format". Offer a default if a sensible one exists.
Simple lesson: res.format picks the handler. default covers everyone else.
Redirects
Redirect = an answer with a 3xx status and a Location header. The client then asks for the new URL.
- 301 Moved Permanently: the page moved for good. Search engines update.
- 302 Found: a temporary move. Express's default.
- 303 See Other: "now GET this page". Used after a form POST. See Input Processing and Validation.
- 307 Temporary Redirect: like 302, but the method and body must stay the same.
- 308 Permanent Redirect: like 301, but the method and body must stay the same.
Animation: the client asks for /old-menu, gets 301 with Location /menu, then asks for /menu and gets 200
Client
Express
GET /old-menu
301 Location: /menu
GET /menu
200 OK
A redirect is two trips: the client follows the Location header by itself.
res.redirect and res.location
- res.redirect(url) sends 302 with a
Locationheader. - res.redirect(301, url) chooses the status. In Express 5 the status comes first.
- Relative URLs like
/menustay on your site. Absolute URLs likehttps://...can go anywhere. - res.location(url) only sets the
Locationheader. Use it with 201 Created. See Building REST APIs. res.redirect("back")was removed in Express 5. Usereq.get("Referrer") || "/".
- Open Headers to see each
Location. - The lab's client does not follow redirects, so you can see them. Browsers follow them by themselves.
Simple lesson: Pick the code by "permanent or not" and "keep the method or not".
Building Redirect URLs
- Use route paths you control, like
/menu, not text from the user. - Keep the query string when moving a page: users keep their filters.
- req.originalUrl has the full path and query. Swap the path, keep the rest.
- Encode values you add, with
encodeURIComponent. - Spaces and symbols in a URL must be encoded, or the link breaks.
Simple lesson: Keep the user's query, and encode anything you add.
Redirect Safety
Open redirect = your site sends users to any URL an attacker puts in a link.
- A login page often has
?next=/ordersto go back after login. - An attacker sends
?next=https://evil.example/fake-login. - The link starts on your real site, so people trust it.
- Allow only local paths: must start with
/, and must not start with//. - Or keep an allowlist of exact destinations.
//evil.examplelooks local, but browsers treat it as another site. The safe version blocks it.
Never pass req.query, req.body or a header straight into res.redirect. Check it first.
Simple lesson: Only redirect to places you chose.
Reverse Proxies
Reverse proxy = a server in front of your app. Examples: Nginx, a load balancer, Cloudflare.
- The proxy handles HTTPS, then passes plain HTTP to Express.
- So Express sees the proxy's IP address, not the user's.
- It also sees
http, even though the user usedhttps. - Proxies add forwarded headers with the real details:
X-Forwarded-For(client IP),X-Forwarded-Proto(http or https),X-Forwarded-Host(original host).
trust proxy
- By default, Express ignores forwarded headers. Anyone could fake them.
- app.set("trust proxy", 1) = trust exactly one proxy in front.
- A number = how many proxy hops to trust.
- An address list, like
"loopback, 10.0.0.1"= trust only those proxies. - true = trust everything. Only safe if nothing can reach Express directly.
With trust set, these change:
- req.ip becomes the real client IP from
X-Forwarded-For. - req.ips lists the addresses from the trusted proxies.
- req.protocol and req.secure follow
X-Forwarded-Proto. - req.hostname follows
X-Forwarded-Host.
- With
1, Express trusts one hop: the last address the proxy added. - In the second request, the client added a fake
1.2.3.4first. With1, it is ignored. - Change it to
true, and the fake address becomesreq.ip. That is the danger.
Proxies and Cookies
- A proxy usually does the HTTPS part. This is HTTPS termination.
- Express then sees plain
http, sosecurecookies would never be sent. - Set
trust proxy, soreq.securebecomestruefromX-Forwarded-Proto. - express-session also has a
proxy: trueoption for the same reason. See Cookies and Sessions.
Match your setting to your real setup. One Nginx in front: 1. Cloudflare plus a load balancer: often 2, or a list of their addresses. Check your host's docs.
Simple lesson: Trust exactly the proxies you have. No fewer, no more.
Proxy Safety
- Wrong number of hops lets users fake their IP. Rate limits and logs break.
- true with Express reachable directly lets anyone fake
httpsand IPs. - No trust at all behind a proxy makes every user look like one IP, and breaks secure cookies.
- Never use forwarded headers for security decisions unless trust is set correctly.
- Keep the Express port private, so all traffic must pass the proxy.
Simple lesson: Wrong proxy trust silently breaks security. Set it on purpose.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
res.redirect(url, 301) | Status can go last | Express 5 wants (301, url) | Wrong status or an error |
| 302 for a page that moved forever | Any redirect works | Use 301 or 308 | Search engines keep the old URL |
| 302 after a form POST | 302 always means GET | Use 303 See Other | Some clients repeat the POST |
res.redirect(req.query.next) | Users only send good links | Allow only local paths | Open redirect phishing |
trust proxy = true everywhere | Trusting is harmless | Trust the real hop count | Faked IPs and protocols |
No trust proxy behind Nginx | Express sees the real user | It sees the proxy | One IP for all users, secure cookies lost |
res.format with no default | Clients always match | Add a sensible default | Unexpected 406 answers |
Check Yourself
Exercises
What to Remember
- Clients state preferences with
Accept,Accept-Languageand friends. res.formatandreq.acceptspick the format. No match means 406.- Redirects: 301/308 permanent, 302/307 temporary, 303 after a form POST.
- In Express 5, the status comes first:
res.redirect(301, url). - Only redirect to local paths or an allowlist.
- Set
trust proxyto your real proxy count, soreq.ip,req.protocoland secure cookies work.
Next: Calling APIs, Background Jobs and Real-Time. You will call other services, run slow work later and push live updates.