# --- reverse proxy (HTTPS front -> HTTPS backend) ---
ProxyRequests Off
SSLProxyEngine On
# Try first with Off (most apps are happier). If your backend needs the
# original Host, flip it back to On.
ProxyPreserveHost Off
AllowEncodedSlashes NoDecode
# Forward scheme and client IP
RequestHeader set X-Forwarded-Proto expr=%{REQUEST_SCHEME}
RequestHeader append X-Forwarded-For %{REMOTE_ADDR}e
# IMPORTANT: use ProxyPass (not ProxyPassMatch) and include trailing slashes
# so paths and queries are preserved. "nocanon" avoids re-encoding that can
# trigger 400s on some apps.
ProxyPass "/" "https://100.230.50.22:14001/" nocanon retry=0 timeout=5
ProxyPassReverse "/" "https://100.230.50.22:14001/"
# If you have big cookies (SSO), bump limits to avoid 400 on large headers
#LimitRequestFieldSize 32768
ProxyRequests Off
Meaning: Disables forward proxy mode (where Apache would proxy arbitrary requests to any host requested by the client).
Why: You’re running a reverse proxy for a specific backend, not an open forward proxy.
Impact: Prevents Apache from being abused as an open relay.
SSLProxyEngine On
Meaning: Allows Apache to make HTTPS connections to backends (your upstream server) instead of only HTTP.
Why: Without this, if you try to ProxyPass to an https:// target, Apache will refuse and log:
AH00961: HTTPS proxy requested but SSLProxyEngine disabled
Impact: Enables Apache to negotiate TLS with your backend (https://100.230.50.22:14001/ in your case).
ProxyPreserveHost Off
Meaning: Controls what Host: header Apache sends to the backend.
Off → Apache sends the hostname from your ProxyPass target (100.230.50.22:14001).
On → Apache sends the original hostname from the client request (test.com:14001).
Why: Many apps are picky about the Host header.
If your backend app only responds to its own configured hostname (e.g., its IP or internal FQDN), use Off.
If your backend expects to see the public host (e.g., for virtual hosting, routing, or SSL name matching), use On.
Impact: Choosing the wrong value is a common cause of HTTP 400 from backends.
AllowEncodedSlashes NoDecode
Meaning: Lets Apache pass encoded slashes (%2F) in URLs to the backend without decoding them.
Why:
By default, Apache rejects %2F as potentially unsafe (it treats it like a directory separator).
Some apps need %2F in path segments (e.g., IDs containing slashes, base64 blobs).
Impact: Prevents Apache from rejecting such URLs with a 400 or rewriting them in ways that break the backend.
"/" (no trailing slash on backend)
ProxyPass "/" "https://backend.example.com"
No trailing / on the backend URL means Apache does not insert a / before appending the remainder.
This can produce ugly or broken URLs:
Request: /foo
→ Backend sees: https://backend.example.comfoo (note missing slash)
This is a common cause of HTTP 400 or 404 from the backend.
Quotes around / or the backend URL make no difference here.
Whether the backend URL ends with / does make a difference — it determines how Apache joins the incoming path to the backend path.
You switched from ProxyPassMatch to ProxyPass.
Your old line
ProxyPassMatch / https://100.230.50.22:443/
matches only the leading / and (because there’s no capture like ^(.*)$) can drop or warp the rest of the path. That’s why some URLs turned into “file not found.”
With:
ProxyPass "/" "https://100.230.50.22:443/" ...
Apache does a simple prefix map and preserves the entire remainder of the path.
You added a trailing slash on the backend URL.
Trailing slashes control how Apache concatenates paths:
ProxyPass "/" "https://…/"
/images/logo.png → backend sees /images/logo.png ✅
ProxyPass "/" "https://…" (no slash)
/images/logo.png → backend sees images/logo.png ❌ (missing /)
You kept nocanon, so Apache doesn’t re-encode the URL.
Without nocanon, Apache may re-encode %2F, +, etc., which can break app routing and cause 404/400 on certain endpoints.
So the magic isn’t “https://148.230.50.22:443/ fixed it,” it’s ProxyPass + trailing slash + nocanon that fixed your path preservation.
# each config file need to have log and location mode set
# needed
ErrorLog /usr/local/apache2/custom_log/test.com.error.log
CustomLog /usr/local/apache2/custom_log/test.com-ssl-443.access.log combined
# # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
# # error, crit, alert, emerg.
# # It is also possible to configure the loglevel for particular
# # modules, e.g.
# #LogLevel info ssl:war
LogLevel proxy:debug ssl:info
# for every byte log
# log in error.log
# only enable every byte log for debugging
# LoadModule dumpio_module modules/mod_dumpio.so
# DumpIOInput On
# DumpIOOutput On
# LogLevel dumpio:trace7
# special proxy handling for wss(websocket)
# Keep these modules loaded: proxy, proxy_http, proxy_wstunnel, ssl
ProxyRequests Off
SSLProxyEngine On
# Try first with Off (most apps are happier). If your backend needs the
# original Host, flip it back to On.
ProxyPreserveHost On
AllowEncodedSlashes NoDecode
RequestHeader set X-Forwarded-Proto expr=%{REQUEST_SCHEME}
RequestHeader append X-Forwarded-For %{REMOTE_ADDR}e
# Prefer HTTP/1.1 (WS uses Upgrade). Start with h2 disabled to remove that variable:
Protocols http/1.1
# Don't silently fall back to HTTP if Upgrade fails
ProxyWebsocketFallbackToProxyHttp Off
# If your backend has a proper hostname on its cert, USE IT here (best practice):
# replace backend.example.com with your backend FQDN that matches its certificate
# Otherwise keep the IP but read the TLS notes below.
# Map BOTH forms (no trailing slash, and trailing slash)
ProxyPass "/dest/websockify" "wss://100.230.50.22:443/dest/websockify" retry=0 timeout=600
ProxyPassReverse "/dest/websockify" "wss://100.230.50.22:443/dest/websockify"
ProxyPass "/dest/websockify/" "wss://100.230.50.22:443/dest/websockify/" retry=0 timeout=600
ProxyPassReverse "/dest/websockify/" "wss://100.230.50.22:443/dest/websockify/"
# --- everything else over HTTPS ---
ProxyPass "/" "https://148.230.50.22:443/" nocanon retry=0 timeout=30
ProxyPassReverse "/" "https://148.230.50.22:443/"