Routing and bypass
Every request outside the jobs API goes through the same steps: pick the upstream, decide whether it is queued or sent direct, then forward it with the prefix removed.
Prefix matching
A prefix matches when the request path equals it, or starts with it followed by /. That keeps /api from matching /api2. The prefix / matches every path.
| Request path | /api | /files | / (only upstream) |
|---|---|---|---|
/api/users | match | no | match |
/api | match | no | match |
/api2/data | no | no | match |
/other | no | no | match |
Startup validation refuses nested prefixes and refuses / when there is more than one upstream, so at most one upstream can match a path. A path that matches none gets 502 Bad Gateway with X-Proxq-Source: proxq.
Prefix stripping
The matched prefix is removed before forwarding, and the query string is kept. When nothing is left, the path becomes /.
| Request | Prefix | Forwarded as |
|---|---|---|
GET /api/users?page=1 | /api | GET /users?page=1 |
GET /api | /api | GET / |
POST /uploads/img.png | /uploads | POST /img.png |
GET /anything | / | GET /anything |
Upstream URL with a path
The upstream url can carry a path. The stripped request path is appended to it, and a trailing / on the url is ignored.
upstreams:
- prefix: "/files"
url: "http://storage:9000/bucket/data"
| Request | Upstream receives |
|---|---|
GET /files/img.png | GET http://storage:9000/bucket/data/img.png |
GET /files | GET http://storage:9000/bucket/data/ |
Direct proxy bypass
Some requests skip the queue and go straight to the upstream. proxq checks, in this order:
| Condition | Why |
|---|---|
WebSocket upgrade: Connection: upgrade and Upgrade: websocket | A two-way connection cannot be queued |
Transfer-Encoding: chunked | The size is unknown and could be huge |
Content-Length over directProxyThreshold | No point buffering a 1 GB upload into Redis |
The upstream’s pathFilter says so | You asked for it |
The threshold check is off when directProxyThreshold is negative. Set to 0 or left out, it is 10 MiB; see upstream settings.
Path filter
pathFilter.patterns are Go regular expressions matched against the request path as the client sent it, before the prefix is stripped. An upstream without patterns never bypasses on path.
Blacklist (the default): matching paths skip the queue.
upstreams:
- prefix: "/api"
url: "http://backend:3000"
pathFilter:
mode: "blacklist"
patterns:
- "^/api/auth"
- "^/api/health"
Auth and health go straight to the backend. Everything else is queued.
Whitelist: only matching paths are queued.
upstreams:
- prefix: "/api"
url: "http://backend:3000"
pathFilter:
mode: "whitelist"
patterns:
- "^/api/reports"
- "^/api/exports"
Reports and exports are queued. Everything else goes straight through. Any mode other than whitelist acts as blacklist.
How bypassed requests reach the upstream
directProxyMode picks how a bypassed request travels.
proxy (the default): proxq reverse-proxies the request and streams in both directions. The client never sees the upstream URL, and the response is the upstream’s own, without X-Proxq-Source. The upstream receives the client’s Host header. Unlike queued requests, direct-proxied requests get no X-Forwarded-For, X-Real-IP or X-Forwarded-Proto from proxq, and incoming X-Forwarded-* headers are removed. If the upstream cannot be reached, proxq answers 502 with X-Proxq-Source: proxq.
redirect: proxq answers 307 Temporary Redirect with X-Proxq-Source: proxq and a Location of the upstream URL plus the stripped path and query. The client must be able to reach the upstream itself, and it re-sends the body there.
WebSocket upgrades are always reverse-proxied, whatever directProxyMode says.
Direct-proxied requests are not cached and are not jobs, so there is nothing to poll. proxq sets no timeout of its own on them.