Repository navigation
Routing and Restrictions
This page covers remote APIs: how a remote is configured, how API Dock forwards requests to it, and how to limit which routes and methods clients can reach. For cookies and secrets, see Authentication and Cookies. For database routes, see SQL Database Support.
# api_dock_config/config.yaml
name: my-api
remotes:
- service1# api_dock_config/remotes/service1.yaml
name: service1
url: https://remote.api.comGET /service1/users/123 is now forwarded to https://remote.api.com/users/123/. Because no routes list is set, every route is allowed.
Each entry under remotes: in the main config names a file in remotes/, next to the main config file, or an entry in remotes/config.yaml (see below):
| Main config entry | Config read |
|---|---|
- service1 |
remotes/service1.yaml, or service1 in remotes/config.yaml
|
- versioned_service |
every remotes/versioned_service/<version>.yaml, plus the versions of versioned_service in remotes/config.yaml
|
The URL slug is the file's name: (it falls back to the entry name if the file can't be read). For versioned remotes the version is the first path segment (/versioned_service/1.0/... or /versioned_service/latest/...). See Versioning.
| Key | Required | Meaning |
|---|---|---|
name |
yes | Slug used in the URL (/<name>/...) |
url |
yes | Base URL of the upstream API. Without it, requests return 500
|
description |
no | Notes for people reading the config. API Dock doesn't return it |
routes |
no | Allow-list of routes, plus custom route mappings |
restricted |
no | Block-list of routes and methods |
query_params |
no | Which query parameters are forwarded (see Query parameters) |
cookies |
no | Which client cookies to forward, and cookies to inject (see Authentication and Cookies) |
The main config is read once at startup, so adding a remote to it needs a restart. Remote files and remotes/config.yaml are read again on every request, so edits to them take effect right away.
Instead of one file per remote, remotes can be defined in one shared file. Each key under remotes is a remote name, and its value holds the same keys as a remote file:
# api_dock_config/remotes/config.yaml
remotes:
weather: # unversioned: /weather/...
url: https://api.weather.example.com
restricted:
- route: "*"
method: delete
core: # one version: /core/0.5.0/... and /core/latest/...
version: "0.5.0"
url: https://api.example.com
wolf: # several versions; other keys are defaults
description: Wolves API
versions:
- version: "1.0"
url: https://api.wolves.example.com
- version: "0.1"
url: https://beta.api.wolves.example.com# api_dock_config/config.yaml
remotes:
- weather
- core
- wolf- The key is the URL name, so entries don't need a
name:. - A remote file wins over an entry with the same name, and a version file (
remotes/wolf/1.0.yaml) wins over the same inline version. Other versions from both places are combined. See Versioning. - Entries are served only when listed in the main config's
remotes:. - Startup fails if the file is malformed or a listed entry has no
url. -
configis reserved: don't name a remote fileremotes/config.yaml. - A remote's versions can come from a lookup:
versions: {from: <lookup>, version: "{{row.version}}", url: "{{row.url}}"}. Lookup values in aurlmust match the lookup'sallowprefixes.
API Dock handles /<remote>/<path> for GET, POST, PUT, DELETE and PATCH. Other methods get a 405.
-
Remote and version. An unknown remote returns
404. On a versioned remote, a missing or unknown version returns404. A request to the bare remote (/versioned_service/) returns{"versions": [...]}. -
Allow and block lists. The path after the remote (and after the version) is checked against
routesandrestricted. A blocked route returns403. -
Custom mapping. If a
remote_routemapping matches, the path is rewritten. -
URL. The URL is
url+/+ path. Withadd_trailing_slash: true(the default), a trailing/is added when the path doesn't already end with one. A request to the remote root (/service1/) goes tourlwith no slash added. -
Query string. Query parameters are filtered by
query_params. Repeated keys such as?id=1&id=2are forwarded with every value. -
Headers and body. All request headers are forwarded except
Hostand these hop-by-hop headers:Connection,Keep-Alive,Proxy-Authorization,TE,Trailers,Transfer-Encoding,Upgrade.Content-Lengthis also dropped and recalculated.Authorizationis forwarded. The client'sCookieheader is not: the upstreamCookieheader holds only what the remote'scookiessetting allows (none if it isn't set; see Authentication and Cookies). Before api_dock 0.10.0 the rawCookieheader was forwarded. The body is sent forPOST,PUTandPATCH.
The upstream response comes back with its status code, body and headers, including 4xx and 5xx responses. API Dock drops hop-by-hop headers and Content-Length. Headers such as Cache-Control, ETag, Location and Content-Disposition are kept.
| FastAPI (default) | Flask | |
|---|---|---|
| Response body | Streamed to the client as it arrives | Read fully into memory, then sent |
Content-Encoding |
Kept: compressed bytes are passed through unchanged | Dropped: the body is sent decompressed |
Several upstream Set-Cookie headers reach the client as separate headers (before api_dock 0.10.0 they were joined into one).
These go under settings: in the main config.
| Setting | Default | Effect |
|---|---|---|
add_trailing_slash |
true |
Add a trailing / to forwarded paths. This avoids redirects from APIs that expect one. Set it to false for APIs that don't |
follow_redirects |
true |
true: API Dock follows upstream redirects. false: the 3xx response and its Location are passed to the client, which is useful for presigned S3 URLs |
timeout |
10 |
Upstream timeout in seconds. null or false disables it. A timeout returns 502
|
base_path |
none | Also serve the API under a prefix. See Python API and Deployment |
If a remote has a routes list, only matching routes are allowed. Everything else returns 403. Without a list, every route is allowed (subject to restricted).
routes:
- users # /users, any method
- users/{{user_id}} # /users/123, any method
- route: reports/{{}}
method: get # GET only
- route: reports/{{}}
method: post # POST too-
String entries allow every method. To limit methods, use dict entries with
method:, or block methods withrestricted. - A dict entry without
methodalso matches every method. Methods are case-insensitive. - The root of a remote (
/service1/) is always allowed. - A
routeslist in the main config is used for remotes that don't have their own. A remote's ownroutesreplaces the main list (they aren't merged).
restricted:
- admin/* # admin and everything below it
- users/{{}}/private # one exact shape
- route: "*"
method: delete # DELETE on every routerestricted in the main config and restricted in the remote file are combined: a route blocked by either is blocked. Restrictions are checked even when a route is in the allow-list. Main-config routes and restricted apply to remotes only. Database routes ignore them.
The same matching is used by routes, restricted and query_params:
| Pattern | Matches |
|---|---|
users |
exactly users
|
users/{{user_id}}, users/{{}}, users/*
|
users/ plus exactly one segment |
admin/* (trailing /*) |
admin, admin/x, admin/x/y, ... (prefix match) |
"*" (string) |
any single-segment route (foo, not foo/bar) |
{route: "*", method: delete} |
every route, for that method |
A segment matches only when the whole segment is a placeholder. user-{{id}} is a literal segment, not a placeholder.
A dict entry with remote_route sends a matching request to a different upstream path:
name: legacy_api
url: https://old.example.com
routes:
# 1. allow the incoming route (path without the remote name)
- route: users/{{user_id}}/profile
method: get
# 2. map it; `route` starts with the remote name (or {{route_name}})
- route: "{{route_name}}/users/{{user_id}}/profile"
remote_route: legacy/user-info/{{user_id}}
method: getGET /legacy_api/users/42/profile is forwarded to https://old.example.com/legacy/user-info/42/.
Mappings currently have these rules (checked against config.py and by running them):
-
The mapping's
routemust start with the remote's name, either written out (legacy_api/users/...) or as{{route_name}}. A mapping written asusers/{{user_id}}/profilenever matches, and the request is forwarded unchanged. -
The incoming route also needs its own allow entry (entry 1 above). Mapping entries live in
routes, so the allow-list is always on, and a mapping entry alone doesn't allow the incoming path. Without entry 1, the request returns403. - Use
{{name}}(or{name}) inrouteandremote_route. Variables can be reordered:route: "{{route_name}}/search/{{category}}/{{term}}"withremote_route: api/search/{{term}}/in/{{category}}works. -
remote_routecan also use{{route_name}}and{{cookies.<name>}}(the request's cookie). Values are filled in once, so a value containing{{...}}isn't filled in again. - A query string in
remote_route(v2/items?kind=a) is sent with the request, before the forwarded query parameters, and the trailing slash (withadd_trailing_slash: true) goes on the path. - (Before api_dock 0.10.0,
{{route_name}}/{{cookies.<name>}}inremote_routecame out wrapped in braces, and a query string there was dropped.) -
method:on a mapping limits which method is rewritten. Other methods allowed by the allow-list are forwarded unmapped.
By default every query parameter is forwarded. A query_params key on the remote, or on a dict route, filters them by name:
query_params: [page, limit] # remote default: forward only these
routes:
- users
- route: search/{{term}}
method: get
query_params: [q] # this route: forward only q
- route: export
query_params: false # this route: forward none| Value | Effect |
|---|---|
| not set | forward all |
true |
forward all |
false |
forward none |
| list of names | forward only those names (with all their values) |
The first routes entry that matches the request decides. If it's a dict with query_params, that setting wins and replaces the remote-level one. Otherwise (no key, or a string entry), the remote-level query_params applies. Put route-level query_params on the allow entry, not on a remote_route mapping, because mapping patterns include the remote name and don't match here.
Database routes use a different query_params (SQL fragments). See Query Parameters.
| Status | When |
|---|---|
403 |
Route blocked by routes or restricted: {"error": "Route 'x' not allowed for remote 'r'"}
|
404 |
Unknown remote, unknown version, or a path that isn't /<remote>/...
|
405 |
Method other than GET, POST, PUT, DELETE, PATCH |
500 |
Remote file has no url
|
502 |
Upstream unreachable or timed out: {"error": "Error connecting to remote API"} (the reason is in the server log, not the response) |
| upstream's | Any response the upstream sends, passed through unchanged |
Getting started
Remote APIs
Databases
- SQL Database Support
- Query Parameters
- Conditional SQL
- Shared Database Config
- Cross-Schema Queries
- PostgreSQL
- Lookups
Serving
Developing