Application: router
HTTP router in front of GOV.UK to proxy to backend servers on a single domain.
- GitHub
- router
- Ownership
- #govuk-platform-engineering-team owns the repo. #govuk-platform-support receives automated alerts for this repo.
- Category
- Supporting apps
README
GOV.UK Router is an HTTP reverse proxy built on top of triemux.
How router loads routes
Router loads its routing table from Content Store’s PostgreSQL database (or optionally from a flat file) into a trie data structure for fast path lookups.
Router can reload routes without restarting:
- Automatically via PostgreSQL’s
LISTEN/NOTIFYmechanism - Periodic schedule
- Manually via the API server
Internally these use a Go channel to send reload requests that causes Router to reload from content-store's PostgreSQL database.
Routes
Routes can be one of two types:
-
exact: The path must match exactly (e.g. the exact route
/governmentonly matches a request for/government) -
prefix: The path prefix must match (e.g. the prefix route
/governmentmatches requests for/government,/government/policies, etc.)
The route type and URL path determine which route gets matched to a particular request.
Router maintains two separate tries:
- Exact path matches
- Prefix matches
Once a request comes in, Router uses the URL path to first check for an exact match, then falls back to the longest prefix match.
Suppose we have the following routes:
- Prefix route on
/foo - Exact route on
/foo/bar - Exact route on
/bar
Then Router will:
- Returns
404if a request is made for the children of an exact route (e.g./bar/foo/). - Match on the prefix route if the request is made for
/foo - Match on the exact route if the request is made for
/foo/bar - Match on the prefix route if the request is made for
/foo/bar/bazas there is no matching exact route
See route_selection_test.go for more cases.
Handling
Routes have a schemaName property:
- Backend
- Redirect
- Gone
Once a request is matched to a route, Router uses the schemaName property to determine how the request should be handled.
There are 3 handler types to handle a request:
- backend: Reverse proxies the request to a backend application server
-
redirect: Returns an HTTP
301redirect to a new location -
gone: Returns an HTTP
410Gone response for deleted content
Note: some Gone routes are also handled by the backend handler.
Router otherwise:
- serves
503if no routes are loaded - serves
404if the route can’t be found
Redirect routes
Redirect routes have a flag that is used to determine whether the URL path in the request should be preserved.
If the source path is /source and the redirect target is /target then the target URL will preserve the path as follows:
https://source.example.com/target/path/subpath?q1=a&q2=b
Otherwise the URL will be:
https://source.example.com/target/
Redirect routes will only redirect to a lowercase route if the URL path is in all caps (e.g. /GOVERNMENT/GUIDANCE will redirect to /government/guidance).
For details on the route data structure and handler configuration, see docs/data-structure.md.
Request flow
graph LR;
A[Fastly]-->B[Router Load Balancer];
B[Router Load Balancer]-->C[Router nginx];
C[Router nginx]-->D[Router];
D[Router]-->E[Backend];
Router’s load balancer adds the following headers:
X-Forwarded-ForX-Forwarded-ProtoX-Forwarded-Port
Router doesn’t proxy redirect and gone routes to a backend but simply returns the response to the client.
Draft stack
The draft stack consists of ‘draft’ deployments of Router, content store and backends.
Here the request passes through an authenticating proxy before it hits draft router:
graph LR;
A[Authenticating Proxy Load Balancer]-->B[Authenticating Proxy nginx];
B[Authenticating Proxy nginx]-->C[Authenticating Proxy];
C[Authenticating Proxy]-->D[Draft Router nginx];
D[Draft Router nginx]-->E[Draft Router];
E[Draft Router]-->F[Draft backend];
In addition to the headers added by the load balancer authenticating proxy adds the following headers:
X_GOVUK_AUTHENTICATED_USER_ORGANISATIONX_GOVUK_AUTHENTICATED_USER-
X-Forwarded-HostreplacesHost
As before draft router doesn’t proxy redirect and gone routes to a backend.
Nginx
Router runs an nginx instance that proxies traffic to Router. The configuration for both live and draft stack live in govuk-helm-charts
The nginx instance also provides:
- Healthcheck endpoints
- Static error pages
-
robots.txtandhumans.txt - Google Search Console verification files
- Licensify endpoint
It also sets and hides some HTTP headers.
API server
Router runs two HTTP servers:
- Public server (default
:8080) for handling requests - API server (default
:8081) for admin operations
The API server exposes the following routes inside the cluster:
/reload/healthcheck/memory-stats/metrics
Configuration
Router is configured via environment variables:
| Variable | Default | Description |
|---|---|---|
ROUTER_PUBADDR |
:8080 |
Public request server address |
ROUTER_APIADDR |
:8081 |
API/admin server address |
ROUTER_BACKEND_CONNECT_TIMEOUT |
1s |
Backend connection timeout |
ROUTER_BACKEND_HEADER_TIMEOUT |
20s |
Backend response header timeout |
ROUTER_FRONTEND_READ_TIMEOUT |
60s |
Client request read timeout |
ROUTER_FRONTEND_WRITE_TIMEOUT |
60s |
Client response write timeout |
ROUTER_ROUTE_RELOAD_INTERVAL |
1m |
Periodic route reload interval |
ROUTER_TLS_SKIP_VERIFY |
unset | Skip TLS verification |
ROUTER_DEBUG |
unset | Enable debug logging |
ROUTER_ERROR_LOG |
STDERR |
Error log file path |
ROUTER_ROUTES_FILE |
unset | Load routes from JSONL file instead of PostgreSQL |
CONTENT_STORE_DATABASE_URL |
unset | PostgreSQL connection string |
SENTRY_DSN |
unset | Sentry error tracking DSN |
SENTRY_ENVIRONMENT |
unset | Sentry environment tag |
Backend applications are configured with BACKEND_URL_<backend_id> environment variables:
export BACKEND_URL_frontend=http://localhost:3000
export BACKEND_URL_publisher=http://localhost:3001
Routes reference these backends by their ID (e.g., “frontend”, “publisher”).
Serving routes from a flat file
When ROUTER_ROUTES_FILE is set, Router will load routes from the specified JSONL file (one JSON object per line).
Router will also no longer load routes from PostgreSQL, and periodic route updates are disabled.
Example file:
{"BackendID":"frontend","IncomingPath":"/government","RouteType":"prefix","RedirectTo":null,"SegmentsMode":null,"SchemaName":null,"Details":null}
{"BackendID":null,"IncomingPath":"/old-page","RouteType":"exact","RedirectTo":"/new-page","SegmentsMode":"ignore","SchemaName":"redirect","Details":null}
{"BackendID":null,"IncomingPath":"/deleted","RouteType":"exact","RedirectTo":null,"SegmentsMode":null,"SchemaName":"gone","Details":null}
You can export routes from PostgreSQL to a JSONL file using:
ROUTER_ROUTES_FILE=/path/to/routes.jsonl ./router -export-routes
This can be used to continue serving routes when Content Store’s database is down for maintenance.
For details on how to configure Router to load from a file see docs/how-to-serve-routes-from-flat-file.md.
Technical documentation
Recommended reading: How to Write Go Code
Run the test suite
Checks run automatically on GitHub on PR and push. For faster feedback, you can run the tests locally.
The lint check uses golangci-lint, which you can install via Homebrew or your favourite package manager:
brew install golangci-lint
You can run all tests (some of which need Docker installed) by running:
make test
You can also run just the unit tests or just the integration tests, using the
unit_tests and integration_tests targets. The unit tests don’t need Docker.
The trie and triemux packages have unit tests. To run these on their own:
go test -bench=. ./trie ./triemux
The integration tests need Docker in order to run PostgreSQL. They are intended to cover Router’s overall request handling, error reporting and performance.
You can use --ginkgo.focus <partial regex> to run a subset of the integration
tests, for example:
go test ./integration_tests -v --ginkgo.focus 'redirect should preserve the query string'
See Site tests on how to run the site tests.
Debug output
To see debug messages when running tests, set both the ROUTER_DEBUG and
ROUTER_DEBUG_TESTS environment variables:
ROUTER_DEBUG=1 ROUTER_DEBUG_TESTS=1 make test
Update the dependencies
This project uses Go Modules to vendor its dependencies. To update the dependencies:
-
Update all the dependencies, including test dependencies, in your working copy:
make update_deps -
Check for any errors and commit.
git commit -- go.{mod,sum} vendor -
Run the Router test suite. If you need to fix a failing test, keep your changes in separate commits to the
go get/go modcommit. -
Run the tests for all dependencies:
go test all- If there are failures, look into each one and determine whether it needs fixing.
- If anything under
vendor/needs changing then either raise a PR with the upstream project or revert to a set of versions that work together. Onlygo getandgo modshould touch files invendor/.
-
Raise a PR.
Further documentation
- Data structure
- Original thinking behind the router
- Example of adding a metric using the Go prometheus client library
- Site tests
- Triemux
- Trie
Team
GOV.UK Platform Engineering team looks after this repo. If you’re inside GDS, you can find us in #govuk-platform-engineering or view our kanban board.