# Reverse proxy

URL: https://scalws.com/docs/howto/reverse-proxy/
Source: `docs/howto/reverse-proxy.md` in the ScalWS repository

Put ScalWS in front of an HTTP application, send an `/api` path to a second upstream, and
control the `Host` header and WebSockets.

## Prerequisites

* ScalWS installed, with `include: sites.d/*.yaml` in `/etc/scalws/scalws.yaml`.
* The upstream applications running and reachable from the server, for example a web
  front end on `127.0.0.1:3000` and an API on `127.0.0.1:4000` or a Unix socket.

Upstreams are spoken to over HTTP/1.1, as `http://host[:port]`, `https://host[:port]` or
`unix:/absolute/path`. Works under both scalws-controller and `scalwsd.service`.

## The site file

```sh
sudo scalwsctl site add example.com --proxy http://127.0.0.1:3000 --www alias
```

Then add the API route:

```yaml
# /etc/scalws/sites.d/example.com.yaml
domain: example.com
www: alias
proxy: http://127.0.0.1:3000
routes:
  - path: /api
    runtime:
      type: proxy
      upstream: "http://127.0.0.1:4000"
      response_timeout: 120s
  - path: /healthz
    runtime: {type: respond, body: "ok\n"}
https_redirect: true
```

* [`proxy`](https://scalws.com/docs/reference/site-file/#proxy): the default upstream, as a URL or as a map
  of settings.
* [`routes[].path`](/docs/reference/site-file/#routes-.path): `/api` matches `/api` and
  everything below it on whole segments (`/api/users`, not `/apikeys`). The longest
  matching prefix wins.
* The path is **not stripped**: the API receives `/api/users`, not `/users`. An upstream
  URL with a path (`http://127.0.0.1:4000/v1`) is refused. If the API expects paths
  without the prefix, set the route's
  [`rewrite`](https://scalws.com/docs/reference/site-file/#routes-.rewrite) (`rewrite: {prefix: /}` sends
  `/api/users` as `/users`), or configure the prefix in the application.
* [`response_timeout`](https://scalws.com/docs/reference/site-file/#routes-.runtime.proxy.response_timeout):
  time for the upstream to send response headers (default `60s`);
  [`connect_timeout`](https://scalws.com/docs/reference/site-file/#routes-.runtime.proxy.connect_timeout)
  defaults to `5s`.

## The Host header: `preserve_host`

[`preserve_host`](https://scalws.com/docs/reference/site-file/#proxy.preserve_host) is `true` by default:
the upstream sees the client's `Host` (`example.com`). Set it to `false` when the upstream
expects its own name, for example a virtual host on another server or a hosted service:

```yaml
domain: example.com
proxy:
  upstream: "https://app.example.org"
  preserve_host: false
```

With `false` the request carries `Host: app.example.org`. For `https://` upstreams the
host is also the TLS server name; [`tls_ca_file`](https://scalws.com/docs/reference/site-file/#proxy.tls_ca_file)
adds a CA bundle for a private CA.

Headers the upstream gets:

* `X-Forwarded-For` (the client address) and `X-Forwarded-Proto` (`http` or `https`),
  set by ScalWS. Values the client sent in `Forwarded`, `X-Forwarded-*` and `X-Real-IP`
  are discarded.
* `X-Request-Id`, the server's request id.
* Hop-by-hop headers are removed in both directions.

## WebSockets

[`websocket`](https://scalws.com/docs/reference/site-file/#proxy.websocket) is `true` by default: HTTP/1.1
`Upgrade: websocket` requests are tunnelled to the upstream on a dedicated connection.
Nothing to configure for a typical application. Turn it off on a route that must never
upgrade:

```yaml
domain: example.com
proxy: http://127.0.0.1:3000
routes:
  - path: /api
    runtime: {type: proxy, upstream: "unix:/run/api/api.sock", websocket: false}
```

Server-wide limits apply to all tunnels:
[`server.limits.max_upgraded_connections`](https://scalws.com/docs/reference/server-limits/#server.limits.max_upgraded_connections)
(default 10000, beyond it `503`) and
[`server.limits.upgraded_idle_timeout`](https://scalws.com/docs/reference/server-limits/#server.limits.upgraded_idle_timeout)
(default `300s` without traffic in either direction). WebSockets over HTTP/3 are not
supported.

## Apply and check

```sh
sudo scalwsctl check
sudo scalws-controller reload        # or: sudo scalwsctl reload under scalwsd.service

curl -sI -H 'Host: example.com' http://127.0.0.1/            # front end
curl -sI -H 'Host: example.com' http://127.0.0.1/api/ping    # API
curl -s  -H 'Host: example.com' http://127.0.0.1/healthz     # ok
```

A WebSocket handshake through the proxy answers `101 Switching Protocols` when the
upstream accepts it at that path (`/socket` here):

```sh
curl -si -H 'Host: example.com' -H 'Connection: Upgrade' -H 'Upgrade: websocket' \
     -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
     http://127.0.0.1/socket | head -1
```

## Common problems

* **502 Bad Gateway**: the upstream refused the connection or sent invalid HTTP. The log
  shows `upstream request failed` with `reason` `connect` or `protocol`. Test the upstream
  with `curl` from the server.
* **504 Gateway Timeout**: no response headers within `response_timeout`. Raise it on the
  route that needs it.
* **The application builds wrong URLs or redirects to the upstream address**: it gets the
  upstream's name in `Host`; keep `preserve_host: true`, or make the application use
  `X-Forwarded-Proto` for the scheme.
* **The API gets 404 for every path**: it expects paths without `/api`; the prefix is not
  stripped unless the route sets `rewrite: {prefix: /}`.
* **`runtime.upstream: ...` in `scalwsctl check`**: the upstream has a path, user
  information or port 0.
* **WebSockets fail**: `websocket: false` on that route, or the
  `max_upgraded_connections` limit is reached.
* **A Unix socket upstream gives `Permission denied`**: user `scalws` must be able to
  connect to the socket.

See also: [applications: proxy](https://scalws.com/docs/guide/applications/#proxy),
[routes](https://scalws.com/docs/guide/applications/#routes),
[troubleshooting](https://scalws.com/docs/guide/troubleshooting/#http-errors-clients-see).
