Skip to content

Commit 8b1977b

Browse files
Document features for proxies
1 parent 1ebdba4 commit 8b1977b

File tree

9 files changed

+327
-1
lines changed

9 files changed

+327
-1
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ venv
44
.env
55
site.zip
66
deploy
7+
out
8+
.local
79

810
# temporary
911
rodi

blacksheep/docs/behind-proxies.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
Production web applications are commonly deployed behind HTTP proxies. While
2+
many modern web services use cloud platforms that abstract away the need to
3+
manage HTTP proxies, there are still scenarios where managing load balancing
4+
and proxy rules is necessary. This is especially true when deploying
5+
applications using platforms like [Kubernetes](https://kubernetes.io/).
6+
7+
Deploying web applications behind proxies often requires configuring routing
8+
based on the properties of incoming web requests. The most common examples
9+
include routing based on the HTTP **host** header, the prefix of the **URL
10+
path**, or a combination of both.
11+
12+
This page provides an overview of the features provided by BlackSheep to handle
13+
these scenarios.
14+
15+
### Routing based on hostnames
16+
17+
```mermaid
18+
graph LR
19+
client1["Client 1"] -->|HTTP GET https://orders.neoteroi.xyz/order/123| proxy["Routing rules"]
20+
client2["Client 2"] -->|HTTP POST https://orders.neoteroi.xyz/order| proxy["Routing rules"]
21+
client3["Client 3"] -->|HTTP GET https://users.neoteroi.xyz/user/123| proxy["Routing rules"]
22+
23+
subgraph "HTTP Proxy"
24+
direction TB
25+
proxy
26+
end
27+
28+
subgraph "Servers"
29+
A["Orders Web API<br>orders.neoteroi.xyz"]
30+
B["Users Web API<br>users.neoteroi.xyz"]
31+
C["Consents Web API<br>consents.neoteroi.xyz"]
32+
end
33+
34+
proxy -->|&nbsp;orders.neoteroi.xyz&nbsp;| A
35+
proxy -->|&nbsp;users.neoteroi.xyz&nbsp;| B
36+
proxy -->|&nbsp;consents.neoteroi.xyz&nbsp;| C
37+
38+
%% Note
39+
classDef note stroke:#000,stroke-width:1px;
40+
note["Example: *.neoteroi.xyz is the wildcard domain used by an HTTP&nbsp;Proxy.
41+
Several domains are configured to point to the same proxy.<br>
42+
Requests are routed to different backend services based on subdomains."]:::note
43+
proxy -.-> note
44+
```
45+
46+
Routing based solely on the **host** header generally does not introduce
47+
complications for backend web applications. However, it does require additional
48+
maintenance to manage multiple domain names and TLS settings, and routing
49+
rules.
50+
51+
### Routing based on paths
52+
53+
Path-based routing allows a proxy server to forward requests to different
54+
backend services based on a prefix of the URL path. This is particularly useful
55+
when hosting multiple applications or services under the same domain.
56+
57+
```mermaid
58+
graph LR
59+
client1["Client 1"] -->|HTTP&nbsp;GET&nbsp;https:&sol;&sol;api&period;neoteroi&period;xyz&sol;order&sol;123| proxy["Routing rules"]
60+
client2["Client 2"] -->|HTTP&nbsp;POST&nbsp;https:&sol;&sol;api&period;neoteroi&period;xyz&sol;order| proxy["Routing rules"]
61+
client3["Client 3"] -->|HTTP&nbsp;GET&nbsp;https:&sol;&sol;api&period;neoteroi&period;xyz&sol;user&sol;123| proxy["Routing rules"]
62+
63+
subgraph "HTTP Proxy"
64+
direction TB
65+
proxy
66+
end
67+
68+
subgraph "Servers"
69+
A["Orders Web API"]
70+
B["Users Web API"]
71+
C["Consents Web API"]
72+
end
73+
74+
proxy -->|&nbsp;/orders/*&nbsp;| A
75+
proxy -->|&nbsp;/users/*&nbsp;| B
76+
proxy -->|&nbsp;/consents/*&nbsp;| C
77+
78+
%% Note
79+
classDef note stroke:#000,stroke-width:1px;
80+
note["Example: api.neoteroi.xyz is the domain of an HTTP&nbsp;Proxy.<br>
81+
Depending on the first portion of the URL path,<br/>the HTTP Proxy forwards the request to the appropriate server."]:::note
82+
proxy -.-> note
83+
```
84+
85+
When deploying behind proxies in this manner, it is crucial to ensure that the
86+
application properly handles being exposed at a specific path. While this works
87+
well for most REST APIs, it can lead to complications with redirects and for
88+
applications that include user interfaces.
89+
90+
The following diagram illustrates the problem of redirects, if the path prefix
91+
is not handled properly.
92+
93+
```mermaid
94+
sequenceDiagram
95+
autonumber
96+
actor User
97+
participant Proxy as HTTP Proxy<br>(Exposes /example/)
98+
participant Backend as Backend Server<br>(Exposed at /)
99+
100+
User->>Proxy: HTTP GET https://example.com/example/dashboard
101+
Proxy->>Backend: HTTP GET /dashboard
102+
Backend-->>Proxy: HTTP 302 Redirect to /sign-in
103+
Proxy-->>User: HTTP 302 Redirect to /sign-in
104+
105+
note over User: The user is redirected to<br>https://example.com/sign-in,<br>which is incorrect because<br>the prefix /example/ is missing.
106+
107+
User->>Proxy: HTTP GET https://example.com/sign-in
108+
Proxy-->>User: HTTP 404 Not Found
109+
```
110+
111+
/// details | The example of API Gateway
112+
type: info
113+
114+
API Gateways like [AWS API Gateway](https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/api-routing-path.html)
115+
and [Azure API Management](https://learn.microsoft.com/en-us/azure/api-management/api-management-key-concepts)
116+
use path based routing to expose many APIs behind the same domain name.
117+
Path based routing generally does not cause complications for REST APIs, but
118+
likely causes complications for web apps serving HTML documents and
119+
implementing interactive sign-in.
120+
121+
///
122+
123+
---
124+
125+
`BlackSheep` offers two ways to deal with this scenario:
126+
127+
- One approach, defined by the `ASGI` specification, involves specifying a
128+
`root_path` in the `ASGI` server. This information is passed in the scope of
129+
web requests. This method is ideal for those who prefer not to modify the
130+
path at which web servers handle requests, and to configure the proxy server
131+
to strip the extra prefix when forwarding requests to backend services
132+
(applying URL rewrite).
133+
- The second approach involves configuring a prefix in the application router
134+
to globally change the prefix of all request handlers. The global prefix can
135+
also be set using the environment variable `APP_ROUTE_PREFIX`. This method
136+
assumes that modifying the path handled by the web server is desirable to
137+
align it with the path handled by the HTTP proxy server, and it is ideal
138+
when using URL rewrite is not easy.
139+
140+
For both options, `BlackSheep` handles the information provided by `root_path`
141+
or the application router prefix in some specific ways.
142+
For example, the `get_absolute_url_to_path` defined in `blacksheep.messages`
143+
will handle the information and return an absolute URL to the server
144+
according to both scenarios.
145+
146+
| Feature | Description |
147+
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
148+
| `request.base_path` | Returns the `base_path` of a web request, when the ASGI scope includes a `root_path`, or a route prefix is used. |
149+
| `blacksheep.messages.get_absolute_url_to_path` | Returns an absolute URL path to a given destination, including the current `root_path` or route prefix. Useful when working with redirects. |
150+
| OpenAPI Documentation | Since version `2.1.0`, it uses relative links to serve the OpenAPI Specification files (YAML and JSON), and relative paths to support any path prefix. |
151+
152+
/// details | Jinja2 template helper
153+
type: tip
154+
155+
The BlackSheep MVC template includes an example of helper function to
156+
[render absolute paths](https://github.com/Neoteroi/BlackSheep-MVC/blob/88b0672a0696d4bef4775203fae086173fd9b0fc/%7B%7Bcookiecutter.project_name%7D%7D/app/templating.py#L26)
157+
in Jinja templates.
158+
159+
///

blacksheep/docs/cache-control.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ For example, a middleware that disables cache-control by default can be defined
115115
class NoCacheControlMiddleware(CacheControlMiddleware):
116116
"""
117117
Disable client caching globally, by default, setting a
118-
Cache-Contro: no-cache, no-store for all responses.
118+
Cache-Control: no-cache, no-store for all responses.
119119
"""
120120

121121
def __init__(self) -> None:

blacksheep/docs/css/extra.css

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,51 @@
1+
html {
2+
overflow-y: scroll;
3+
}
4+
5+
/* For large screens, make better use of the horizontal space */
6+
@media screen and (min-width: 2000px) {
7+
.md-grid {
8+
max-width: 98%;
9+
}
10+
11+
.md-sidebar {
12+
width: auto;
13+
min-width: 15%;
14+
}
15+
16+
body.fullscreen #fullscreen-form label {
17+
display: none !important;
18+
}
19+
}
20+
21+
@media screen and (min-width: 1000px) {
22+
html.fullscreen {
23+
.md-grid {
24+
max-width: 98%;
25+
}
26+
27+
.md-sidebar {
28+
width: auto;
29+
min-width: 15%;
30+
}
31+
}
32+
}
33+
34+
#fullscreen-form label {
35+
display: none;
36+
}
37+
38+
html:not(.fullscreen) #full-screen {
39+
display: inline-block !important;
40+
}
41+
42+
html.fullscreen #full-screen-exit {
43+
display: inline-block !important;
44+
}
45+
46+
html.fullscreen #full-screen {
47+
display: none !important;
48+
}
149

250
[data-md-color-scheme="default"] .md-typeset [type="checkbox"]:checked + .task-list-indicator::before {
351
background-color: #2d319f;

blacksheep/docs/js/fullscreen.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
document.addEventListener("DOMContentLoaded", function () {
2+
function setFullScreen() {
3+
localStorage.setItem("FULLSCREEN", "Y")
4+
document.documentElement.classList.add("fullscreen");
5+
}
6+
function exitFullScreen() {
7+
localStorage.setItem("FULLSCREEN", "N")
8+
document.documentElement.classList.remove("fullscreen");
9+
}
10+
11+
// Select all radio inputs with the name "__fullscreen"
12+
const fullscreenRadios = document.querySelectorAll('input[name="__fullscreen"]');
13+
14+
// Add a change event listener to each radio input
15+
fullscreenRadios.forEach(function (radio) {
16+
radio.addEventListener("change", function () {
17+
if (radio.checked) {
18+
if (radio.id === "__fullscreen") {
19+
setFullScreen();
20+
} else if (radio.id === "__fullscreen_no") {
21+
exitFullScreen();
22+
}
23+
}
24+
});
25+
});
26+
});

blacksheep/docs/settings.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This page describes:
1414
| APP_ENV | Settings | This environment variable is read to determine the environment of the application. For more information, refer to [_Defining application environment_](/blacksheep/settings/#defining-application-environment). |
1515
| APP_SHOW_ERROR_DETAILS | Settings | If "1" or "true", configures the application to display web pages with error details in case of HTTP 500 Internal Server Error. |
1616
| APP_MOUNT_AUTO_EVENTS | Settings | If "1" or "true", automatically binds lifecycle events of mounted apps between children and parents BlackSheep applications. |
17+
| APP_ROUTE_PREFIX | Settings | Allows configuring a global prefix for all routes handled by the application. For more information, refer to: [Behind proxies](/blacksheep/behind-proxies/). |
1718
| APP_SECRET_<i>i</i> | Secrets | Allows configuring the secrets used by the application to protect data. |
1819
| BLACKSHEEP_SECRET_PREFIX | Secrets | Allows specifying the prefix of environment variables used to configure application secrets, defaults to "APP_SECRET" if not specified. |
1920

blacksheep/mkdocs.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ nav:
3939
- Compression: compression.md
4040
- Examples:
4141
- Using Marshmallow: examples/marshmallow.md
42+
- Production deployments:
43+
- Behind proxies: behind-proxies.md
4244
- Settings: settings.md
4345
- Binders: binders.md
4446
- Background tasks: background-tasks.md
@@ -83,6 +85,9 @@ extra_css:
8385
- css/neoteroi.css
8486
- css/extra.css?v=20221120
8587

88+
extra_javascript:
89+
- js/fullscreen.js
90+
8691
plugins:
8792
- search
8893
- neoteroi.contribs:
@@ -94,6 +99,7 @@ markdown_extensions:
9499
- admonition
95100
- markdown.extensions.codehilite:
96101
guess_lang: false
102+
- pymdownx.blocks.details
97103
- pymdownx.superfences:
98104
custom_fences:
99105
- name: mermaid

blacksheep/overrides/main.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@
1919
<meta name="twitter:title" content="{{ title }}">
2020
<meta name="twitter:description" content="{{ config.site_description }}">
2121
<meta name="twitter:image" content="{{ image }}">
22+
<script>
23+
(function () {
24+
var fullscreen = localStorage.getItem("FULLSCREEN") || "N";
25+
if (fullscreen === "Y") {
26+
document.documentElement.classList.add("fullscreen");
27+
}
28+
})();
29+
</script>
2230
{% endblock %}
2331
{% block content %}
2432
{% if not config.extra.is_current_version %}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
{#-
2+
This file was automatically generated - do not edit
3+
-#}
4+
{% set class = "md-header" %}
5+
{% if "navigation.tabs.sticky" in features %}
6+
{% set class = class ~ " md-header--shadow md-header--lifted" %}
7+
{% elif "navigation.tabs" not in features %}
8+
{% set class = class ~ " md-header--shadow" %}
9+
{% endif %}
10+
<header class="{{ class }}" data-md-component="header">
11+
<nav class="md-header__inner md-grid" aria-label="{{ lang.t('header') }}">
12+
<a href="{{ config.extra.homepage | d(nav.homepage.url, true) | url }}" title="{{ config.site_name | e }}" class="md-header__button md-logo" aria-label="{{ config.site_name }}" data-md-component="logo">
13+
{% include "partials/logo.html" %}
14+
</a>
15+
<label class="md-header__button md-icon" for="__drawer">
16+
{% set icon = config.theme.icon.menu or "material/menu" %}
17+
{% include ".icons/" ~ icon ~ ".svg" %}
18+
</label>
19+
<div class="md-header__title" data-md-component="header-title">
20+
<div class="md-header__ellipsis">
21+
<div class="md-header__topic">
22+
<span class="md-ellipsis">
23+
{{ config.site_name }}
24+
</span>
25+
</div>
26+
<div class="md-header__topic" data-md-component="header-topic">
27+
<span class="md-ellipsis">
28+
{% if page.meta and page.meta.title %}
29+
{{ page.meta.title }}
30+
{% else %}
31+
{{ page.title }}
32+
{% endif %}
33+
</span>
34+
</div>
35+
</div>
36+
</div>
37+
<form id="fullscreen-form" class="md-header__option" data-md-component="fullscreen">
38+
<input class="md-option" aria-label="Enter full screen" type="radio" name="__fullscreen" id="__fullscreen">
39+
<label title="Full screen" class="md-header__button md-icon" id="full-screen" for="__fullscreen">
40+
{% include ".icons/material/fullscreen.svg" %}
41+
</label>
42+
<input class="md-option" aria-label="Exit full screen" type="radio" name="__fullscreen" id="__fullscreen_no">
43+
<label title="Exit full screen" class="md-header__button md-icon" id="full-screen-exit" for="__fullscreen_no">
44+
{% include ".icons/material/fullscreen-exit.svg" %}
45+
</label>
46+
</form>
47+
{% if config.theme.palette %}
48+
{% if not config.theme.palette is mapping %}
49+
{% include "partials/palette.html" %}
50+
{% endif %}
51+
{% endif %}
52+
{% if not config.theme.palette is mapping %}
53+
{% include "partials/javascripts/palette.html" %}
54+
{% endif %}
55+
{% if config.extra.alternate %}
56+
{% include "partials/alternate.html" %}
57+
{% endif %}
58+
{% if "material/search" in config.plugins %}
59+
<label class="md-header__button md-icon" for="__search">
60+
{% set icon = config.theme.icon.search or "material/magnify" %}
61+
{% include ".icons/" ~ icon ~ ".svg" %}
62+
</label>
63+
{% include "partials/search.html" %}
64+
{% endif %}
65+
{% if config.repo_url %}
66+
<div class="md-header__source">
67+
{% include "partials/source.html" %}
68+
</div>
69+
{% endif %}
70+
</nav>
71+
{% if "navigation.tabs.sticky" in features %}
72+
{% if "navigation.tabs" in features %}
73+
{% include "partials/tabs.html" %}
74+
{% endif %}
75+
{% endif %}
76+
</header>

0 commit comments

Comments
 (0)