This commit is contained in:
Mateusz Gruszczyński committed 2026-10-10 00:18:06 +02:00
1 parent ad95bfb050
commit 1431dd47f0
62 files changed
+1360 -198

No files matched your search

+18
View File
@@ -3,6 +3,24 @@
This describes implemented source, not a verified Rust runtime. See
[TEST_REPORT.md](TEST_REPORT.md).
## 0.29.7: show diagnostic headers only on cache HIT
Set `cache.expose_only_hits: true` globally (Settings > Cache) to emit
`X-Cache`, `X-Cache-Hits`, `X-Cache-Reason`, `Cache-Status` and
`X-Cache-Resident-Age` only when the response is actually served from
RustProxy cache. MISS and BYPASS continue to be recorded in internal metrics
and access logs but these diagnostic headers are absent from those responses.
Cached stale/revalidated/range/conditional responses use the HIT outcome and
continue to show cache diagnostics. Individual vhosts can override the global
flag with `vhosts[].cache.expose_only_hits: true | false | null` (`null`
inherits). Changing the flag alone does not purge cached objects.
The independent `Via` proxy-chain header can still appear on MISS/BYPASS when
`cache.via_header` is configured. Set it to `null` to turn off that header.
Existing `expose_status`, `expose_hits`, `expose_cache_status` and
`expose_resident_age` switches still select which individual HIT headers are
emitted.
## 0.19.1 cache identity safety
The frontend scheme is part of the primary cache identity. `http://host/path` and
+6
View File
@@ -7,6 +7,12 @@
# Changelog
## 0.29.7 same-version `E0308` source correction
- Defer Reverse Proxy error interception from the upstream `Response<hyper::body::Incoming>` to the adapted `Response<axum::body::Body>` in `success_prepare.rs`. This preserves the H2 WebSocket tunnel's `Incoming` body type and ensures replacement still precedes response transformation and cache admission. Reapply response rules and enforce `no-store` for replaced application errors.
- Change health supervisor member collection to own `Arc<Member>` and spawn probes with `Arc::clone(member)`, meeting Tokio's owned `'static` task requirement.
- Extend the application-error UI/source regression check with the body-adaptation ordering, no-store and Arc ownership assertions. Offline checks passed; Rust compilation is not available in the packaging environment.
## 0.26.7
### Backend client lifecycle and cache diagnostics
+11
View File
@@ -116,6 +116,17 @@ RustProxy 0.29.1 can mark a Custom location as `internal`. An internal location
This is an internal request handoff, not an HTTP redirect. The original method, URI/query and request context are retained. The fallback target re-enters normal access/header/cache/compression/response processing. Fallback targets must be enabled internal locations in the same vhost; validation rejects cycles and runtime dispatch is depth-limited. See [0.29.1 implementation note](V0.29.1_IMPLEMENTATION.md).
### Replace origin error bodies without an internal location (0.29.7)
The independent, optional **Vhost → Pages → Intercept application error responses**
setting (`pages.intercept_statuses`, for example `[404, 503]`) replaces a
backend or Web Server/PHP origin's error *body* with the configured vhost
error page, then global error page, or a built-in page. It retains the HTTP
status and sets `Cache-Control: no-store`. Unlike `error_fallback`, it does
**not** forward the request to a different backend. The existing internal
`error_fallback` action has priority and its final response remains intact.
See [Web Server – application HTTP error interception](WEB_SERVER.md#intercept-application-http-errors-0297).
## What is intentionally not an Nginx parser
Custom locations do **not** execute arbitrary Nginx directives or configuration snippets. RustProxy does not currently implement location-level `try_files`, per-location filesystem aliases/roots, embedded scripting, arbitrary `if` expressions or arbitrary module directives. Static roots are defined only by reusable Web Services. Use the explicit UI controls and the documented JSON schema instead; unknown fields are rejected on save.
+89
View File
@@ -1,5 +1,10 @@
# PHP-FPM / FastCGI for the Web Server
**Separate pools for distinct Web Services:** Reusing the same PHP-FPM socket
with different `PHP_ADMIN_VALUE` maps can leak `open_basedir` restrictions into
a different virtual host. See the 0.29.7 guide
[PHP_FPM_ISOLATION.md](PHP_FPM_ISOLATION.md) for remediation and tests.
RustProxy 0.25 can execute configured PHP extensions through a long-running PHP-FPM pool while preserving the reusable Web Service architecture.
## Architecture
@@ -44,6 +49,90 @@ Configuration example:
Unspecified PHP fields inherit the safe serde defaults. A service without
`php_profile_id` uses its own `php` object as in earlier versions.
## Where to configure each PHP setting (0.29.7)
Both editors expose the **same fields and validation**:
1. **Web Server → Web Service → PHP-FPM → Local configuration:** set values on one service (`web_services[].php`). Select **Local configuration** in the profile selector first.
2. **Web Server → PHP-FPM profiles → Add/Edit profile:** set shared values on a profile (`php_fpm_profiles[].php`), then attach it in a Web Service's PHP-FPM tab (`php_profile_id`). Editing the profile changes every attached service. Per-service `php` values remain saved for later detach but do **not** override an attached profile.
3. **Actual PHP-FPM pool config** (`php-fpm.d/*.conf` or distribution equivalent): the pool must exist and listen on the configured socket. Isolate restrictive `open_basedir`/`php_admin_value` settings in *separate pools and sockets*, not by alternating request-time `PHP_ADMIN_VALUE` on shared workers. RustProxy does not launch PHP-FPM.
| UI field (JSON property) | What to enter / sample | Notes |
| --- | --- | --- |
| Enable PHP-FPM / FastCGI (`enabled`) | Checked for PHP apps | Off means PHP is not executed. Never serve PHP source. |
| PHP-FPM socket (`endpoint`) | `unix:/run/php/php_wordpress.sock`, `tcp:127.0.0.1:9000`, `tcp:[::1]:9000` | Only local Unix sockets or TCP loopback **IP**, not hostnames. Socket ownership and permissions must allow RustProxy access. |
| PHP extensions (`extensions`) | `php, phtml` | Comma-separated, no leading dots; at most 16. Only listed extensions are executable. |
| Enable PHP PATH_INFO splitting (`path_info`) | Checked for Roundcube `/static.php/skins/...` | Splits an existing PHP script path and its trailing URL; off for ordinary WordPress unless needed. |
| Forward Authorization (`pass_authorization`) | Checked when API / Basic / Bearer auth is used | Sends request `Authorization` to `HTTP_AUTHORIZATION` in FastCGI. |
| Connection timeout (`connect_timeout_ms`) | `1000` = 1 second | Integer 50–60000 milliseconds. |
| Request timeout (`request_timeout_ms`) | `30000` = 30 seconds | Integer 100–3600000 milliseconds for the full FastCGI request. |
| Max simultaneous PHP requests (`max_concurrent_requests`) | `64` | Integer 1–4096 per Web Service; excess is rejected with HTTP 503. |
| Request-body limit (`max_request_body_bytes`) | `67108864` = 64 MiB; `0` = no additional limit | Integer 0–8589934592 **bytes**, independent of PHP `post_max_size`. |
| Response-header limit (`max_response_header_bytes`) | `65536` = 64 KiB | Integer 4096–1048576 bytes. |
| PHP-FPM STDERR log limit (`stderr_log_bytes`) | `4096`; `0` = disabled | Integer 0–65536 bytes collected per request. |
| Custom FastCGI parameters (`custom_params`) | `APP_ENV = production` | One static `NAME = value` per line. Names A-Z/0-9/underscore, beginning with a letter. No core CGI, HTTP_* or PHP control params. |
| PHP INI overrides (`php_values` / `PHP_VALUE`) | `memory_limit = 256M` | One php.ini `directive = value` per line. PHP decides which directives can change at this scope. |
| Administrator PHP INI overrides (`php_admin_values` / `PHP_ADMIN_VALUE`) | `open_basedir = /var/www/webmail/:/tmp/` | Trusted operator only; for security isolation prefer dedicated PHP-FPM pool configuration. Do not define the same directive in both INI maps. |
**Syntax in the three multiline fields is not JSON:** use `key = value`, a separate line per entry; **do not** wrap it in `PHP_VALUE=...` or `php_admin_value[...]` inside the RustProxy editor. Those latter directives belong to the *native PHP-FPM pool configuration*, not the RustProxy FastCGI request fields. Empty values and control characters are rejected. Up to 64 entries per map, maximum 8192 bytes per value and 128 KiB combined.
### Ready-to-use WordPress and Roundcube examples
**WordPress — Web Service `/var/www/linuxiarz.pl/`, dedicated FPM socket:**
```text
PHP enabled: checked
PHP PATH_INFO: unchecked
Endpoint: unix:/run/php/php_wordpress.sock
PHP extensions: php
PHP_VALUE:
memory_limit = 256M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 60
PHP_ADMIN_VALUE: leave empty (or set security restrictions in WordPress's own FPM pool)
```
Use `index.php` in **Index files**, plus `try_files` lines `$uri`, `$uri/`, `/index.php?$query_string` in **Routing**. The WordPress pool must be allowed to read its `wp-config.php`, `wp-includes` and `wp-content` directories.
**Roundcube — Web Service `/var/www/webmail/public_html`, separate FPM socket:**
```text
PHP enabled: checked
PHP PATH_INFO: checked
Endpoint: unix:/run/php/php_roundcube.sock
PHP extensions: php
PHP_VALUE:
memory_limit = 256M
upload_max_filesize = 64M
post_max_size = 64M
PHP_ADMIN_VALUE: prefer the native Roundcube PHP-FPM pool instead; e.g.:
open_basedir = /var/www/webmail/:/usr/lib/php/:/tmp/:/etc/enigma
```
Add additional permitted external paths only if Roundcube's actual dependencies need them. Roundcube assets such as `/static.php/skins/elastic/...` require PATH_INFO splitting. Restart/recycle workers after separating pools; using a different RustProxy profile but the **same socket** does not isolate the worker state. See [pool isolation](PHP_FPM_ISOLATION.md).
### FastCGI vs php.ini examples
```text
# Custom FastCGI parameters — application-specific static environment:
APP_ENV = production
APP_DEBUG = 0
# PHP_VALUE — regular PHP-level options:
memory_limit = 256M
max_execution_time = 60
error_reporting = E_ALL
log_errors = On
display_errors = Off
date.timezone = Europe/Warsaw
# PHP_ADMIN_VALUE — privileged operator-level options:
open_basedir = /var/www/webmail/:/usr/lib/php/:/tmp/
```
The `#` comments above are explanations separating fields; **do not paste them** into the corresponding form inputs. Boolean INI directives commonly accept `On`/`Off`, size directives can accept `K`/`M`/`G` and timeouts use seconds. The actual PHP/FPM version determines supported directives and permitted scopes.
## Runtime model
RustProxy implements the FastCGI protocol directly. It does not spawn `php`, `php-cgi` or shell processes for individual requests.
+31
View File
@@ -0,0 +1,31 @@
# Dedicated PHP-FPM pools for distinct Web Services
PHP-FPM may retain request-supplied `PHP_VALUE` and `PHP_ADMIN_VALUE` directives in long-lived workers. When two applications use one PHP-FPM socket but different INI overrides, a setting such as `open_basedir` can be inherited by the next request, even when RustProxy routes the request to the correct Web Service. **RustProxy cannot provide process-level isolation inside a shared external PHP-FPM worker.**
## Affected operator configuration (0.29.6)
- WordPress Web Service `blog-linuxiarz-pl` uses document root `/var/www/linuxiarz.pl/` and endpoint `unix:/run/php/php_rustproxy.sock`, without an `open_basedir` override.
- Roundcube Web Service `webmail-linuxiarz-pl` uses document root `/var/www/webmail/public_html` and the **same** endpoint. Its FastCGI `PHP_ADMIN_VALUE` includes `open_basedir=/var/www/webmail/:/usr/lib/php/:/tmp/:/etc/enigma:...`.
- WordPress PHP-FPM stderr confirms the Roundcube `open_basedir` at the WordPress file check. As a result, `wp-config.php` is not visible and WordPress redirects to `/wp-admin/setup-config.php` before triggering fatal errors. This is **not** a cache MISS defect.
## Required operator remediation
1. Back up the current FPM configuration. Provision separate, access-restricted PHP-FPM pools under the PHP-FPM service manager used by your system (NixOS configuration when applicable). Use distinct Unix sockets, for example `/run/php/php_wordpress.sock` and `/run/php/php_roundcube.sock`. Ensure RustProxy's runtime user may connect to both. Do **not** expose raw FastCGI publicly.
2. Configure pool-level `php_admin_value[open_basedir]` for **Roundcube only**, with exactly the filesystem paths it requires, or keep its validated override in RustProxy on a dedicated pool. Keep WordPress in its own pool with appropriate policy. Pool-level directives are preferable to request-supplied administrator directives for security-sensitive settings.
3. In **Web Server > Web Services** set `blog-linuxiarz-pl > PHP-FPM endpoint` to `unix:/run/php/php_wordpress.sock` and `webmail-linuxiarz-pl` to `unix:/run/php/php_roundcube.sock`. Shared PHP-FPM profiles must also refer to the corresponding correct endpoint.
4. Apply PHP-FPM configuration and **restart** the affected PHP-FPM service/pools to retire workers with leaked directives. Save the RustProxy Web Service changes. Check that both web apps respond and that private files remain blocked.
5. Repeat requests in alternating order from both vhosts and inspect PHP-FPM warnings. Suggested tests: `curl -I https://blog.linuxiarz.pl/`, `curl -I https://blog.linuxiarz.pl/wp-login.php`, `curl -I https://webmail.linuxiarz.pl/`. The WordPress home page must no longer redirect to `/wp-admin/setup-config.php` due to `open_basedir`.
If WordPress still redirects to setup-config.php with a **dedicated** pool, check whether `wp-config.php` really exists and is readable by the pool user, the configured pool's own php_admin_value restrictions, and database credentials. Do not fix the issue by using unrestricted `open_basedir=/` globally or by disabling protections on Roundcube.
## New 0.29.7 protection
During configuration compilation, RustProxy emits a warning identifying pairs of Web Services sharing an endpoint with unequal `PHP_VALUE`/`PHP_ADMIN_VALUE` maps. For backward compatibility, the existing configuration remains loadable; the warning **does not** automatically rewrite operator FPM configuration or claim that sharing becomes safe.
## Per-backend health
For an internal 404 response origin, leave `pool_eligible: false`, set `health_check_enabled: false`, and continue routing a named `internal` location to its `backend_id`. If a backend does require separate probes, use an optional per-member `health_check` object (same shape as vhost `load_balancing.health_check`).
## HIT-only cache diagnostic headers
Enable global `cache.expose_only_hits: true`, optionally override each vhost at `vhosts[].cache.expose_only_hits`. HIT-only mode filters RustProxy diagnostic cache headers for MISS/BYPASS without altering internal hit/miss metrics or upstream cache eligibility. `Via` is a separate proxy-header option and can be disabled using `cache.via_header: null`.
+1
View File
@@ -37,6 +37,7 @@ This is the canonical index for the current project documentation. The authentic
- [WEB_SERVER](WEB_SERVER.md) - reusable Web Services, static-file behavior, ranges/validators and document-root security.
- [PHP_FPM](PHP_FPM.md) - implemented FastCGI/PHP-FPM integration, security model and runtime requirements.
- [PHP_FPM_ISOLATION](PHP_FPM_ISOLATION.md) - separate PHP-FPM pools to prevent cross-vhost INI settings leakage.
- [VHOSTS](VHOSTS.md) - vhosts and backend/source behavior.
- [CUSTOM_LOCATIONS](CUSTOM_LOCATIONS.md) - location matching, rewrites and local responses.
- [VHOST_ACCESS](VHOST_ACCESS.md) - access rules applied to vhosts and locations.
+38 -3
View File
@@ -1,4 +1,39 @@
# Verification report - RustProxy 0.29.6
# Verification report - RustProxy 0.29.7
## Same-version 0.29.7 PHP profile UI, Rust warning and error interception review (2026-10-09)
- Move the shared PHP-FPM **Syntax and safety** help button to the bottom of the profile-management dialog, below **Add profile**, with a visible horizontal separator. Keep the service-local help button where it was. The simulated Web Service UI test asserts this placement and that the help modal still opens.
- Remove the unused `mut` in the `web_server::serve` binding (`src/proxy/forward/web_source.rs`).
- Correct application-error interception semantics in both Reverse Proxy and Web Server/PHP-FPM: responses supplied by a backend/service (including cache hits) are explicitly marked and are not overwritten by global/vhost error pages when `pages.intercept_statuses` is disabled. RustProxy-generated routing/upstream failures remain eligible for the configured local error page. Interception is performed before cache admission; selected statuses receive `no-store` even after response-header rules and always retain the application's HTTP status, ignoring a standalone error page's status override. An internal named-location fallback remains terminal and has precedence.
- Rust regression tests cover the opt-in behavior, the marker's effect in the response observer, precedence and preservation of status with a differently configured page status. Source/UI contracts verify both forwarding paths and correct ordering around HTTP/2 WebSocket body adaptation; these are **not** substitutes for live protocol tests.
- `./dev.sh test`: all Rust-independent source, JS, SQL/upgrade, security, documentation and translation checks passed; then exited **127** due to unavailable `cargo`/`rustc`. Rust compilation, unit tests and runtime HTTP/FastCGI/WebSocket acceptance have **not** been performed here. No configuration schema change or version bump.
## 0.29.7 same-version compile error corrections (2026-10-09)
- Correct the three operator-reported `E0308` mismatches. Reverse Proxy origin interception now receives an `axum::body::Body` response only **after** adapting the upstream `Response<hyper::body::Incoming>`, preserving the original `Incoming` body expected by the HTTP/2 WebSocket tunnel wrapper. The operation remains before transformation and cache admission.
- Reapply configured response-header rules to an intercepted page, then restore `Cache-Control: no-store` so the admin page cannot accidentally enter shared cache through a custom header policy. Internal named error fallbacks retain precedence.
- Build owned `Arc<Member>` candidates for the active health-check supervisor and clone the `Arc` into each `JoinSet::spawn` task, rather than attempting to spawn a future carrying `&Arc<Member>`.
- Static/UI/source/SQL/upgrade/security/documentation checks passed via `./dev.sh test` up to the Rust guard (exit 127). `./dev.sh check` exited 127 before compilation due to missing Rust/Cargo. **These are source-level corrections, not a verified Rust build.** Operator should run `./dev.sh check` and `./dev.sh test` on a Cargo-equipped host before deployment.
## 0.29.7 in-version PHP UX and origin error interception (2026-10-09)
- Both Web Service local PHP fields and shared PHP-FPM profiles now expose per-field examples, units, bounded syntax, PATH_INFO/Authorization/PHP enablement info tooltips and a common detailed help dialog; all localized through language packs. Configuration semantics remain unchanged.
- New optional `vhosts[].pages.intercept_statuses`: supported unique HTTP error codes are validated; selected application-origin responses are replaced by vhost/global/built-in RustProxy pages before admission to cache, preserve the HTTP status by default, and carry `Cache-Control: no-store`. The existing internal error fallback runs first. Empty/default list leaves existing origin error responses intact.
- Added Rust source-level regression tests for default preservation, replacement, headers, precedence and malformed codes plus JS form behavior checks. `./dev.sh test` ran all pre-Cargo contracts (including Web Service PHP profile editing, origin-interception roundtrip, offline migration and security checks) successfully but exited **127** at the missing `cargo` guard. `./dev.sh check` also exited **127** for that reason. Rust compilation, Rust unit execution, and live Web Server/FastCGI/backend routing remain **unverified**.
## 0.29.7 backend/cache/FPM policy review (2026-10-09)
- Backend monitoring now has an explicit per-member switch and optional policy; tests cover backwards-compatible defaults, serialization, inherited Host and validation. Membership in the balanced pool remains a separate setting.
- `cache.expose_only_hits` defaults to false globally and may be overridden at a vhost. Tests cover HIT/MISS/BYPASS suppression, origin diagnostic removal, independently configured Via and cache identity stability. Actual wire behavior requires Cargo/runtime tests.
- **PHP limitation:** user-supplied WordPress and Roundcube services both target `unix:/run/php/php_rustproxy.sock`. Only Roundcube passes a restrictive FastCGI `PHP_ADMIN_VALUE: open_basedir`; PHP-FPM may retain this value across worker reuse, causing WordPress to fail. The 302 to setup-config.php is a consequence of missing access to wp-config.php. Added conflict warning, inline guidance and pool-isolation instructions; code changes cannot alter an already-shared PHP-FPM worker's isolation. The operator must provision and select a dedicated Roundcube FPM pool and restart/recycle affected old workers.
- Source changes keep SQLite schema 9/Cold Cache format 3. Compiler-independent tests and verification are reported below; Rust compilation/live HTTP/2/H3/PHP integration is not asserted in this environment.
### 0.29.7 verification summary
- `./dev.sh test`: all Rust-independent contracts, SQLite migration/backup/source-upgrade checks, JS vhost/Web Server regressions, source security/dependency audits, docs and translation checks passed. It exits with code 127 at the Rust/Cargo availability guard, because `rustc` and `cargo` are not installed. This is **not** a successful Rust test-suite run.
- `node tools/checks/backend_cache_0297_ui_test.mjs`: passed; simulates the actual backend form toggles and verifies independent monitoring/pool-eligibility state, per-member health override and cache controls.
- Rust-only regression tests were added but could not execute: effective active/passive member policies and validation, HTTP Host defaults, HIT-only cache headers, per-vhost policy resolution, and cache compatibility identity.
- Operator acceptance remains open: Rust compilation, real PHP-FPM worker isolation on separate sockets, alternating WordPress/Roundcube requests, live 404 fallback, and actual wire-level cache HIT/MISS/BYPASS checks on HTTP/1.1, H2 and H3.
## 0.29.6 FastCGI EOF and error-log routing (2026-10-09)
@@ -112,9 +147,9 @@ This source package is not certified for production deployment.
Installer/update now installs /etc/logrotate.d/rustproxy using the recorded service user/group. Updates repair /etc/profile.d/rustproxy-path.sh rather than warning. logrotate uses copytruncate because RustProxy holds open log descriptors; some log records may be lost during truncate. Rust build and live Debian/systemd acceptance remain unverified in this environment.
## Version identity (0.29.6 source release)
## Version identity (0.29.7 source release)
`Cargo.toml` package version is the canonical release identity (now 0.29.6). Rust uses `CARGO_PKG_VERSION` at build time; `build.rs` substitutes the admin HTML placeholder. Packaging, upgrade and dev scripts read the manifest with `scripts/version.py`. Run `python3 tools/checks/version_contracts.py` to check that executable paths do not hardcode a version. Historical documentation and external dashboards may retain manually managed versions. Rust compilation/runtime verification is still pending.
`Cargo.toml` package version is the canonical release identity (now 0.29.7). Rust uses `CARGO_PKG_VERSION` at build time; `build.rs` substitutes the admin HTML placeholder. Packaging, upgrade and dev scripts read the manifest with `scripts/version.py`. Run `python3 tools/checks/version_contracts.py` to check that executable paths do not hardcode a version. Historical documentation and external dashboards may retain manually managed versions. Rust compilation/runtime verification is still pending.
## Shared PHP-FPM profiles and Web Server cache/transform/Host patch (0.29.4)
+17
View File
@@ -44,6 +44,23 @@ The main vhost `source` may define `error_fallback`, and each Custom location ma
Configured upstream/Web Server failures are intercepted before final response/cache publication. The original request method, URI/query and context are internally dispatched to the target, which then follows normal vhost/location processing. Validation rejects fallback cycles and runtime dispatch is depth-limited. See [V0.29.1_IMPLEMENTATION.md](V0.29.1_IMPLEMENTATION.md) for body replay bounds and an Nginx-style static -> app -> redirector example.
## Per-backend health monitoring (0.29.7)
Each backend has an independent `health_check_enabled` switch (default `true`).
Setting it to `false` excludes that member from both active health probes and
passive health ejection, **without** disabling the member for a named location.
This is useful for an internal 404 handler configured with `pool_eligible: false`
and a location pinned through `backend_id`: the 404 handler never receives
ordinary balanced traffic and does not need health monitoring.
A backend may also define `health_check` with the same fields as
`load_balancing.health_check` to use its own TCP/HTTP/off probe policy, timing,
path, Host and status ranges. Omitting it inherits the vhost policy. A custom
HTTP health check without `host` inherits the vhost Host default even when the
vhost's main health check is TCP. When monitoring is disabled, the backend is
assumed available; connection errors during real requests are still counted and
logged, so monitoring off is not a guarantee of backend reachability.
## Frontend selection and private IP isolation
`frontend.hosts` is a normalized hostname-pattern list. Exact hostnames and a single
+38
View File
@@ -35,6 +35,44 @@ The Web Service editor is split into stable configuration tabs so later features
After the Web Service is saved, attach it under **Vhosts** by selecting `Web Server` and the service ID for the main source or a custom location.
### Intercept application HTTP errors (0.29.7)
Open **Vhosts → Edit vhost → Pages** (not the Web Service PHP tab), enable
**Intercept application error responses**, and enter statuses such as `404, 500, 503`.
This opt-in vhost policy applies to response bodies returned by both **Reverse
Proxy origins** and **Web Server/PHP-FPM origins**. It is configured in
`vhosts[].pages.intercept_statuses`; an omitted/empty list disables it and
preserves the application error HTML. Supported codes: 400, 401, 403, 404,
408, 413, 429, 500, 502, 503, 504.
For each opted-in origin status, RustProxy replaces the *body* using the
vhost's configured error page, then the global error page, then its built-in
error page. The HTTP response status always remains the application's status,
even if the selected page has a custom `status` for standalone error handling.
`Cache-Control: no-store` is set and origin response cookies, compression
metadata and application error HTML are not forwarded. Relevant protocol
headers such as `WWW-Authenticate`, `Allow` and `Retry-After` are retained.
Local RustProxy security/routing failures are not treated as application
responses. If `error_fallback` sends an error to a named internal location,
that response takes precedence and is not overwritten by this option.
```json
{
"id": "my-vhost",
"pages": {
"intercept_statuses": [404, 500, 503],
"errors": {
"404": {"kind": "html", "status": null, "html": "<h1>Not found</h1>"}
}
}
}
```
This is a **vhost fragment**, not a standalone valid vhost object. A page's
`status` override still applies to locally generated RustProxy error pages,
but is ignored when replacing an application's error body. See also
[Custom locations](CUSTOM_LOCATIONS.md).
### Application presets
The editor provides one-shot presets for **Static site, SPA, PHP generic, WordPress, Drupal, Laravel, Symfony, Joomla, PrestaShop and Downloads/file repository**. Applying a preset writes normal Web Service fields; it does not store a hidden CMS mode and every generated field remains editable. Service ID/name, document root and the deployment-specific PHP-FPM endpoint/tuning are preserved. Laravel/Symfony operators should point `document_root` at the application's `public/` directory.