# Redirects, Content Negotiation and Proxies

Course: [Express.js](https://www.devtutorials.ai/learn/expressjs) · Section: Advanced HTTP
Source: https://www.devtutorials.ai/learn/expressjs/advanced-http/redirects-negotiation-proxies

> 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.

## 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.

<figure className="xfig ngc-fig">
<svg viewBox="0 0 560 150" role="img" aria-labelledby="xfig-redir" style={{ width: "100%", maxWidth: 560, height: "auto" }}>
  <title id="xfig-redir">Animation: the client asks for /old-menu, gets 301 with Location /menu, then asks for /menu and gets 200</title>
  <g style={{ fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>
    <rect x="15" y="25" width="100" height="100" rx="12" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 2 }} />
    <text x="65" y="80" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>Client</text>
    <rect x="445" y="25" width="100" height="100" rx="12" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 2 }} />
    <text x="495" y="80" textAnchor="middle" style={{ fill: "var(--ngc-green)" }}>Express</text>
  </g>
  <g style={{ fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700 }}>
    <g opacity="0">
      <rect x="-70" y="-11" width="140" height="22" rx="11" style={{ fill: "var(--ngc-blue)" }} />
      <text x="0" y="4" textAnchor="middle" style={{ fill: "#fff" }}>GET /old-menu</text>
      <animateMotion path="M190 45 L370 45" dur="8s" repeatCount="indefinite" keyPoints="0;0;1;1" keyTimes="0;0.02;0.22;1" calcMode="linear" />
      <animate attributeName="opacity" values="0;1;1;0;0" keyTimes="0;0.02;0.22;0.24;1" dur="8s" repeatCount="indefinite" />
    </g>
    <g opacity="0">
      <rect x="-90" y="-11" width="180" height="22" rx="11" style={{ fill: "var(--ngc-orange)" }} />
      <text x="0" y="4" textAnchor="middle" style={{ fill: "#fff" }}>301 Location: /menu</text>
      <animateMotion path="M370 75 L190 75" dur="8s" repeatCount="indefinite" keyPoints="0;0;1;1" keyTimes="0;0.26;0.46;1" calcMode="linear" />
      <animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.25;0.26;0.46;0.48;1" dur="8s" repeatCount="indefinite" />
    </g>
    <g opacity="0">
      <rect x="-60" y="-11" width="120" height="22" rx="11" style={{ fill: "var(--ngc-blue)" }} />
      <text x="0" y="4" textAnchor="middle" style={{ fill: "#fff" }}>GET /menu</text>
      <animateMotion path="M190 45 L370 45" dur="8s" repeatCount="indefinite" keyPoints="0;0;1;1" keyTimes="0;0.5;0.7;1" calcMode="linear" />
      <animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.49;0.5;0.7;0.72;1" dur="8s" repeatCount="indefinite" />
    </g>
    <g opacity="0">
      <rect x="-50" y="-11" width="100" height="22" rx="11" style={{ fill: "var(--ngc-green)" }} />
      <text x="0" y="4" textAnchor="middle" style={{ fill: "#fff" }}>200 OK</text>
      <animateMotion path="M370 105 L190 105" dur="8s" repeatCount="indefinite" keyPoints="0;0;1;1" keyTimes="0;0.74;0.94;1" calcMode="linear" />
      <animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.73;0.74;0.94;1" dur="8s" repeatCount="indefinite" />
    </g>
  </g>
</svg>
<figcaption>A redirect is two trips: the client follows the Location header by itself.</figcaption>
</figure>

### 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

<div className="tscroll">

| 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 |

</div>

## 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.
