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, Location and X-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.

  1. A parent asks for the notice in Tamil. A teacher asks for it in English. Same notice, different language.
  2. The canteen moved to Block B. A sign on the old door says "Now in Block B".
  3. Visitors sign in at the gate. The guard writes "Visitor from Gate 2" before sending them in.
  4. 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:

  1. How clients ask for formats, languages and encodings.
  2. How to answer with res.format and req.accepts.
  3. Redirect status codes and res.redirect.
  4. How to build safe redirect URLs.
  5. How reverse proxies and trust proxy work.
  6. How to stay safe with forwarded headers.

Content Negotiation

The client says what it prefers in request headers:

  • Accept: formats, such as application/json or text/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.
  • Vary tells caches that the answer depends on Accept-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 like application/json.
  • Express sets Content-Type and Vary: Accept for you.
  • default runs when nothing matches.
  • Without default, Express answers 406 Not Acceptable.
  • One route can serve a web page and an API.
  • image/png matches 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 Location header.
  • res.redirect(301, url) chooses the status. In Express 5 the status comes first.
  • Relative URLs like /menu stay on your site. Absolute URLs like https://... can go anywhere.
  • res.location(url) only sets the Location header. Use it with 201 Created. See Building REST APIs.
  • res.redirect("back") was removed in Express 5. Use req.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=/orders to 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.example looks 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 used https.
  • 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.4 first. With 1, it is ignored.
  • Change it to true, and the fake address becomes req.ip. That is the danger.

Proxies and Cookies

  • A proxy usually does the HTTPS part. This is HTTPS termination.
  • Express then sees plain http, so secure cookies would never be sent.
  • Set trust proxy, so req.secure becomes true from X-Forwarded-Proto.
  • express-session also has a proxy: true option 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 https and 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

MistakeWrong ideaCorrect ideaWhat goes wrong
res.redirect(url, 301)Status can go lastExpress 5 wants (301, url)Wrong status or an error
302 for a page that moved foreverAny redirect worksUse 301 or 308Search engines keep the old URL
302 after a form POST302 always means GETUse 303 See OtherSome clients repeat the POST
res.redirect(req.query.next)Users only send good linksAllow only local pathsOpen redirect phishing
trust proxy = true everywhereTrusting is harmlessTrust the real hop countFaked IPs and protocols
No trust proxy behind NginxExpress sees the real userIt sees the proxyOne IP for all users, secure cookies lost
res.format with no defaultClients always matchAdd a sensible defaultUnexpected 406 answers

Check Yourself

Exercises

What to Remember

  • Clients state preferences with Accept, Accept-Language and friends.
  • res.format and req.accepts pick 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 proxy to your real proxy count, so req.ip, req.protocol and secure cookies work.

Next: Calling APIs, Background Jobs and Real-Time. You will call other services, run slow work later and push live updates.