Summary
The http / mixed inbound currently treats a plain HTTP forward-proxy request too much like a tunnel protocol:
accept TCP connection
-> parse one HTTP proxy request
-> create one Session
-> evaluate route once
-> connect one upstream
-> replay the first normalized request head
-> turn the rest of the TCP connection into a raw byte relay
That lifecycle is correct for CONNECT, SOCKS5 CONNECT, VLESS/Trojan-style streams, etc., where one accepted protocol request establishes one fixed tunnel.
It is not correct for plain HTTP forward-proxy traffic. HTTP/1.1 is a message protocol: a single client-to-proxy TCP connection can carry multiple requests, and each request has its own request target and may require its own routing/policy decision.
The currently reproduced failure is HTTP keep-alive: only the first absolute-form request is parsed and rewritten. A second request on the same proxy connection bypasses HTTP parsing entirely and is forwarded through the already-established upstream as raw bytes.
This breaks normal browser/system-proxy traffic and is reproducible with Vite/VitePress, but the root cause is broader than Vite and broader than request-line rewriting.
Reproduction
The target is reachable through WireGuard and is matched by an IPv4 CIDR route:
Example target:
Using the znet-sink mixed proxy at 127.0.0.1:7890:
curl.exe -v -x http://127.0.0.1:7890 \
http://16.10.68.3:5173/ \
http://16.10.68.3:5173/@vite/client
First request:
> GET http://16.10.68.3:5173/ HTTP/1.1
> Host: 16.10.68.3:5173
> Proxy-Connection: Keep-Alive
< HTTP/1.1 200 OK
< Content-Type: text/html
< Connection: keep-alive
curl then explicitly reuses the same proxy TCP connection:
* Reusing existing http: connection with proxy 127.0.0.1
> GET http://16.10.68.3:5173/@vite/client HTTP/1.1
> Host: 16.10.68.3:5173
> Proxy-Connection: Keep-Alive
The second response is incorrectly the Vite index.html document rather than the module resource:
< HTTP/1.1 200 OK
< Content-Type: text/html
< Content-Length: 521
<!DOCTYPE html>
<html>
...
Browser result:
Failed to load module script: Expected a JavaScript-or-Wasm module script
but the server responded with a MIME type of "text/html".
The expected request reaching the origin is:
GET /@vite/client HTTP/1.1
but the reused connection can deliver the original proxy-form request directly to the origin:
GET http://16.10.68.3:5173/@vite/client HTTP/1.1
Vite then treats the target differently and falls back to index.html.
If 16.10.68.3 is added to the Windows system-proxy bypass list, the site works because the traffic no longer enters the HTTP proxy path. This is therefore not evidence of a DIRECT/WireGuard routing failure. The request reaches the destination correctly when proxy protocol handling is bypassed.
Current implementation and root cause
The relevant flow is currently split across:
protocols/http/src/inbound.rs
crates/proxy/src/adapters/http/inbound.rs
crates/proxy/src/adapters/mixed/inbound.rs
crates/proxy/src/runtime/tcp_ingress/lifecycle/serve.rs
For a plain absolute-form HTTP request:
HttpConnectInbound::accept_request() reads exactly one HTTP request head.
- It parses the request target and constructs a
Session.
- It rewrites the first request line from absolute-form to origin-form and returns those bytes as replay data.
- The HTTP or Mixed adapter wraps the connection in
HttpRequestReplayStream.
context.serve(...) passes the single Session into the generic TCP ingress lifecycle.
serve_inbound() performs fake-IP resolution, URL rewrite, kernel rate-limit selection, Session preparation, route evaluation, outbound selection, tracking, and upstream establishment once.
protocol.relay(...) then owns the connection as a raw bidirectional byte stream.
After step 7, later bytes are no longer interpreted as HTTP proxy messages.
This is the architectural mismatch:
Generic tunnel model
====================
TCP connection
-> one Session
-> one target
-> one route decision
-> one upstream
-> raw relay
HTTP forward-proxy model
========================
TCP proxy connection
-> request #1 -> target/session/route #1
-> request #2 -> target/session/route #2
-> request #3 -> target/session/route #3
-> ...
The existing generic lifecycle therefore cannot provide correct persistent forward-proxy behavior simply by replaying more request bytes.
Confirmed consequences of the current lifecycle
1. Only the first request is normalized
The first absolute-form request is converted correctly:
GET http://example.test/a HTTP/1.1
becomes:
but a later request on the same proxy TCP connection never enters accept_request() and therefore receives no normalization.
This is the directly reproduced #17 failure.
2. Later requests are not routed again
The problem is not limited to request-line rewriting.
Because serve_inbound() evaluates a route only once for the original Session, a later HTTP request on the same client proxy connection does not receive a new:
- target extraction;
- fake-IP resolution;
- URL rewrite;
- route-rule evaluation;
- reject/direct/proxy decision;
- outbound selection;
- route trace;
- logical Session attribution.
For example, a persistent proxy connection must not behave like this:
request #1 -> allowed.example -> DIRECT
request #2 -> blocked.example -> still uses request #1's established DIRECT stream
Whether a particular client reuses one proxy connection across different authorities depends on the client, but the core lifecycle must not rely on same-destination reuse as a correctness or security boundary.
3. Forward HTTP and CONNECT currently share too much lifecycle semantics
These modes need to diverge after parsing.
CONNECT is correctly tunnel-oriented:
CONNECT host:443
-> parse target
-> route once
-> establish upstream
-> send 200 Connection Established
-> raw bidirectional relay
Plain forward HTTP is message-oriented:
GET http://host/a
-> parse request
-> route request
-> normalize request
-> forward HTTP message
-> process response boundary
-> remain in HTTP proxy mode
GET http://host/b
-> parse again
-> route again
-> normalize again
-> ...
A successful plain HTTP request must not silently convert the entire client-proxy connection into an origin tunnel.
Additional confirmed HTTP intermediary correctness gaps
These are related to the same implementation strategy and should be considered while designing the fix. They are not all required to reproduce the Vite failure.
A. Host is not regenerated from the absolute request target
Current normalization changes only the request line. The remaining header bytes are replayed unchanged.
For example:
GET http://allowed.example/path HTTP/1.1
Host: different.example
creates/routs the Session from the absolute request target (allowed.example), while the original Host header can still be sent upstream unchanged.
For an HTTP/1.1 forward proxy, RFC 9112 §3.2 requires the proxy receiving an absolute-form request target to ignore a received Host field and generate Host from the request target.
Reference:
This mismatch can cause the authority used for core routing/policy to differ from the authority interpreted by a virtual-host/CDN/reverse-proxy origin. The exact policy-bypass impact should be covered by a dedicated regression/security test rather than assumed from theory alone.
B. Proxy-specific / connection-specific headers are replayed unchanged
The current replay path does not appear to normalize HTTP intermediary headers. The reproduced request already contains:
Proxy-Connection: Keep-Alive
and that header is currently part of the replayed header block.
Forward-proxy handling should explicitly define behavior for at least:
Connection
Proxy-Connection
Keep-Alive
TE
Trailer
Transfer-Encoding
Upgrade
Proxy-Authorization
Proxy-Authenticate
Connection-specific fields named by Connection also require handling rather than blind forwarding.
References:
C. HTTP message framing is not currently owned by the forward-proxy layer
A complete persistent forward proxy needs message boundaries, not raw TCP boundaries.
At minimum it must correctly reason about:
- request
Content-Length;
- chunked request bodies;
- response
Content-Length;
- chunked responses;
- methods/statuses with no response body (
HEAD, 1xx, 204, 304, etc.);
- connection-close delimited messages;
- malformed/conflicting framing, including
Transfer-Encoding + Content-Length;
- pipelined/sequential requests if supported;
- switching to raw relay only at a valid Upgrade/CONNECT boundary.
Without owning these boundaries, the proxy cannot safely decide where request #1 ends and request #2 begins.
D. Upgrade/WebSocket requires an explicit state transition
For plain forward HTTP, receiving:
does not itself mean the proxy should immediately become a raw tunnel.
The proxy must forward the HTTP handshake and switch to raw relay only after the origin successfully returns the protocol-switch response (normally 101 Switching Protocols).
CONNECT followed by a WebSocket handshake is different: after the CONNECT tunnel is established, the proxy should remain protocol-agnostic and relay bytes normally.
The current Vite failure occurs before HMR can initialize, so WebSocket is not the root cause of the reproduced bug. It is a compatibility boundary that must be tested when the HTTP state machine is changed.
Other implementation defects discovered in the same HTTP/Mixed path
These are concrete code issues but are separable from the core keep-alive state-machine defect.
1. HTTP redirect rewrite behavior differs between http and mixed
The standalone HTTP inbound checks context.select_http_redirect(&session) before entering serve(...).
The Mixed HTTP branch currently parses the HTTP request and goes directly to context.serve(...) without the equivalent redirect check.
Therefore an HTTP request can behave differently depending on whether it enters through http or mixed, even though Mixed is expected to expose the same HTTP semantics after protocol detection.
2. Redirect-rule iteration can stop on the first non-redirect rewrite rule
select_redirect_target() currently contains logic equivalent to:
for rule in rules {
let status = rule.status_code?;
...
}
Because the function returns Option, a rewrite rule with status_code: None causes the entire function to return None immediately instead of continuing to later rules.
Example:
{
"url_rewrite": [
{ "from": "a.example", "to": "b.example" },
{ "from": "c.example", "to": "d.example", "status_code": 302 }
]
}
The second redirect rule cannot be selected because the first normal rewrite terminates redirect lookup.
This should be fixed independently or as part of the same HTTP/Mixed cleanup, with ordering regression tests.
Inbound resource-management concern discovered during analysis
The generic TCP listener spawns a task per accepted connection. The configured relay idle_timeout() is applied after a Session has been parsed/routed and an upstream has been established.
HTTP request-head parsing itself has no outer handshake/first-request deadline in the adapter/runtime path. A peer can therefore connect and provide an incomplete request head very slowly before the normal relay idle timeout applies.
SOCKS5 and other TCP inbound handshakes should also be audited for the same lifecycle gap.
This is broader than #17 and can be split into a separate issue for a generic inbound_handshake_timeout / admission-control primitive. It should not block defining the correct HTTP forward-proxy architecture here.
Parser limitations worth addressing during the HTTP rewrite
These are compatibility/performance issues rather than the primary #17 failure:
- request-head parsing currently reads one byte at a time until
\r\n\r\n;
- the complete request head is capped at 8192 bytes;
- request-line extraction currently performs UTF-8 validation over the complete captured request head rather than treating header field bytes independently;
- absolute-form parsing currently accepts
http:// but not ws://; supported behavior should be intentional and tested;
- unsupported target/scheme cases are currently easy to collapse into generic
405 Method Not Allowed behavior even when the method itself is valid.
These should be revisited if the implementation moves to a real byte-oriented HTTP/1 parser/state machine rather than extending the current replay mechanism.
Required semantic model
The implementation should explicitly separate two modes.
Mode 1: CONNECT tunnel
client TCP connection
-> parse CONNECT authority
-> create Session
-> evaluate route once
-> establish upstream once
-> reply 200
-> raw relay until close/timeout
One client connection == one tunnel Session is appropriate here.
Mode 2: HTTP forward proxy
client proxy TCP connection
-> parse request #1
-> create logical request Session
-> normalize authority/headers
-> evaluate route
-> establish/reuse suitable upstream
-> relay exactly one HTTP request/response transaction
-> parse request #2
-> create logical request Session
-> normalize authority/headers
-> evaluate route again
-> ...
The client-proxy TCP connection is a transport container, not itself the routing identity.
Whether upstream origin connections are reused is an implementation optimization and must not change request-level routing/policy semantics.
Fix strategy
Phase 1 — correctness/safety fix
If implementing a complete persistent HTTP state machine immediately is too large, prefer an explicit one-request-per-client-connection behavior for plain forward HTTP rather than the current implicit tunnel behavior.
A safe temporary strategy can be:
- parse one complete HTTP request;
- normalize request target and required proxy headers;
- evaluate routing for that request;
- forward exactly that request/response transaction;
- ensure the client-side proxy connection closes after the transaction, forcing the next request to establish a new proxy connection;
- keep CONNECT behavior unchanged.
Simply injecting Connection: close into the request without understanding the response boundary is not sufficient by itself; the implementation still needs to know when that transaction is complete before safely accepting/closing the client side.
Phase 2 — persistent HTTP/1.1 forward-proxy state machine
Implement explicit request/response framing and allow persistent client connections while creating request-level routing/session semantics.
This phase should define:
- same-authority keep-alive;
- different-authority requests on one client proxy connection;
- upstream connection reuse policy;
- request/response body framing;
- header normalization;
- redirects/rewrite interaction;
- Upgrade/WebSocket transition;
- cancellation and idle timeout semantics;
- traffic accounting and logical Session records.
Route/session/telemetry expectations
A fix must not only make the browser render correctly.
For every plain HTTP forward request, core should be able to attribute at least:
request target
inbound tag
route decision / route trace
selected outbound
upstream endpoint
principal / rate-limit context where applicable
traffic belonging to the request or clearly documented connection-level accounting
result / failure reason
A later request must not inherit a previous request's target/route merely because the client reused the same proxy TCP connection.
CONNECT should continue to be represented as one tunnel Session.
Regression coverage
Tests should cover both http and mixed inbounds.
Confirmed failure coverage
Routing/policy coverage
Header/authority coverage
Body/framing coverage
CONNECT / Upgrade coverage
HTTP/Mixed parity
Non-goals / do not workaround in znet-sink
Do not solve this by automatically adding DIRECT destinations to the Windows system-proxy bypass list.
These are different semantics:
DIRECT
= traffic enters core, is evaluated by core, and core opens the destination directly
system proxy bypass
= traffic never enters the HTTP/Mixed inbound at all
Changing DIRECT into OS-level bypass would hide the HTTP proxy defect while changing routing, observability, policy, DNS/process behavior, and user expectations.
Acceptance criteria
This issue is complete when the implementation has an explicit, documented distinction between CONNECT tunnel semantics and plain HTTP forward-proxy semantics, and the following are true:
- Plain HTTP forward requests never become an unqualified raw origin tunnel after the first request.
- Every request accepted on a persistent proxy connection receives correct request-target parsing and normalization.
- Every request receives an appropriate independent routing/policy decision.
- Header/authority handling is suitable for an HTTP intermediary rather than replaying the captured header block blindly.
- HTTP message boundaries are handled safely for supported request/response body forms.
- Upgrade switches to raw relay only at a valid protocol-switch boundary.
http and Mixed's HTTP branch expose the same HTTP behavior.
- CONNECT behavior remains unchanged and continues to work as a raw tunnel after establishment.
- Regression tests cover persistent requests, routing separation, normal bodies/framing, and Vite/HMR-style traffic.
The immediate Vite reproduction can be used as the integration proof, but the fix should target the underlying HTTP forward-proxy semantics rather than Vite-specific behavior.
Summary
The
http/mixedinbound currently treats a plain HTTP forward-proxy request too much like a tunnel protocol:That lifecycle is correct for
CONNECT, SOCKS5 CONNECT, VLESS/Trojan-style streams, etc., where one accepted protocol request establishes one fixed tunnel.It is not correct for plain HTTP forward-proxy traffic. HTTP/1.1 is a message protocol: a single client-to-proxy TCP connection can carry multiple requests, and each request has its own request target and may require its own routing/policy decision.
The currently reproduced failure is HTTP keep-alive: only the first absolute-form request is parsed and rewritten. A second request on the same proxy connection bypasses HTTP parsing entirely and is forwarded through the already-established upstream as raw bytes.
This breaks normal browser/system-proxy traffic and is reproducible with Vite/VitePress, but the root cause is broader than Vite and broader than request-line rewriting.
Reproduction
The target is reachable through WireGuard and is matched by an IPv4 CIDR route:
Example target:
Using the znet-sink mixed proxy at
127.0.0.1:7890:First request:
curl then explicitly reuses the same proxy TCP connection:
The second response is incorrectly the Vite
index.htmldocument rather than the module resource:Browser result:
The expected request reaching the origin is:
but the reused connection can deliver the original proxy-form request directly to the origin:
Vite then treats the target differently and falls back to
index.html.If
16.10.68.3is added to the Windows system-proxy bypass list, the site works because the traffic no longer enters the HTTP proxy path. This is therefore not evidence of a DIRECT/WireGuard routing failure. The request reaches the destination correctly when proxy protocol handling is bypassed.Current implementation and root cause
The relevant flow is currently split across:
For a plain absolute-form HTTP request:
HttpConnectInbound::accept_request()reads exactly one HTTP request head.Session.HttpRequestReplayStream.context.serve(...)passes the single Session into the generic TCP ingress lifecycle.serve_inbound()performs fake-IP resolution, URL rewrite, kernel rate-limit selection, Session preparation, route evaluation, outbound selection, tracking, and upstream establishment once.protocol.relay(...)then owns the connection as a raw bidirectional byte stream.After step 7, later bytes are no longer interpreted as HTTP proxy messages.
This is the architectural mismatch:
The existing generic lifecycle therefore cannot provide correct persistent forward-proxy behavior simply by replaying more request bytes.
Confirmed consequences of the current lifecycle
1. Only the first request is normalized
The first absolute-form request is converted correctly:
becomes:
but a later request on the same proxy TCP connection never enters
accept_request()and therefore receives no normalization.This is the directly reproduced #17 failure.
2. Later requests are not routed again
The problem is not limited to request-line rewriting.
Because
serve_inbound()evaluates a route only once for the original Session, a later HTTP request on the same client proxy connection does not receive a new:For example, a persistent proxy connection must not behave like this:
Whether a particular client reuses one proxy connection across different authorities depends on the client, but the core lifecycle must not rely on same-destination reuse as a correctness or security boundary.
3. Forward HTTP and CONNECT currently share too much lifecycle semantics
These modes need to diverge after parsing.
CONNECTis correctly tunnel-oriented:Plain forward HTTP is message-oriented:
A successful plain HTTP request must not silently convert the entire client-proxy connection into an origin tunnel.
Additional confirmed HTTP intermediary correctness gaps
These are related to the same implementation strategy and should be considered while designing the fix. They are not all required to reproduce the Vite failure.
A.
Hostis not regenerated from the absolute request targetCurrent normalization changes only the request line. The remaining header bytes are replayed unchanged.
For example:
creates/routs the Session from the absolute request target (
allowed.example), while the originalHostheader can still be sent upstream unchanged.For an HTTP/1.1 forward proxy, RFC 9112 §3.2 requires the proxy receiving an absolute-form request target to ignore a received Host field and generate Host from the request target.
Reference:
This mismatch can cause the authority used for core routing/policy to differ from the authority interpreted by a virtual-host/CDN/reverse-proxy origin. The exact policy-bypass impact should be covered by a dedicated regression/security test rather than assumed from theory alone.
B. Proxy-specific / connection-specific headers are replayed unchanged
The current replay path does not appear to normalize HTTP intermediary headers. The reproduced request already contains:
Proxy-Connection: Keep-Aliveand that header is currently part of the replayed header block.
Forward-proxy handling should explicitly define behavior for at least:
Connection-specific fields named by
Connectionalso require handling rather than blind forwarding.References:
C. HTTP message framing is not currently owned by the forward-proxy layer
A complete persistent forward proxy needs message boundaries, not raw TCP boundaries.
At minimum it must correctly reason about:
Content-Length;Content-Length;HEAD,1xx,204,304, etc.);Transfer-Encoding+Content-Length;Without owning these boundaries, the proxy cannot safely decide where request #1 ends and request #2 begins.
D. Upgrade/WebSocket requires an explicit state transition
For plain forward HTTP, receiving:
Upgrade: websocketdoes not itself mean the proxy should immediately become a raw tunnel.
The proxy must forward the HTTP handshake and switch to raw relay only after the origin successfully returns the protocol-switch response (normally
101 Switching Protocols).CONNECTfollowed by a WebSocket handshake is different: after the CONNECT tunnel is established, the proxy should remain protocol-agnostic and relay bytes normally.The current Vite failure occurs before HMR can initialize, so WebSocket is not the root cause of the reproduced bug. It is a compatibility boundary that must be tested when the HTTP state machine is changed.
Other implementation defects discovered in the same HTTP/Mixed path
These are concrete code issues but are separable from the core keep-alive state-machine defect.
1. HTTP redirect rewrite behavior differs between
httpandmixedThe standalone HTTP inbound checks
context.select_http_redirect(&session)before enteringserve(...).The Mixed HTTP branch currently parses the HTTP request and goes directly to
context.serve(...)without the equivalent redirect check.Therefore an HTTP request can behave differently depending on whether it enters through
httpormixed, even though Mixed is expected to expose the same HTTP semantics after protocol detection.2. Redirect-rule iteration can stop on the first non-redirect rewrite rule
select_redirect_target()currently contains logic equivalent to:Because the function returns
Option, a rewrite rule withstatus_code: Nonecauses the entire function to returnNoneimmediately instead of continuing to later rules.Example:
{ "url_rewrite": [ { "from": "a.example", "to": "b.example" }, { "from": "c.example", "to": "d.example", "status_code": 302 } ] }The second redirect rule cannot be selected because the first normal rewrite terminates redirect lookup.
This should be fixed independently or as part of the same HTTP/Mixed cleanup, with ordering regression tests.
Inbound resource-management concern discovered during analysis
The generic TCP listener spawns a task per accepted connection. The configured relay
idle_timeout()is applied after a Session has been parsed/routed and an upstream has been established.HTTP request-head parsing itself has no outer handshake/first-request deadline in the adapter/runtime path. A peer can therefore connect and provide an incomplete request head very slowly before the normal relay idle timeout applies.
SOCKS5 and other TCP inbound handshakes should also be audited for the same lifecycle gap.
This is broader than #17 and can be split into a separate issue for a generic
inbound_handshake_timeout/ admission-control primitive. It should not block defining the correct HTTP forward-proxy architecture here.Parser limitations worth addressing during the HTTP rewrite
These are compatibility/performance issues rather than the primary #17 failure:
\r\n\r\n;http://but notws://; supported behavior should be intentional and tested;405 Method Not Allowedbehavior even when the method itself is valid.These should be revisited if the implementation moves to a real byte-oriented HTTP/1 parser/state machine rather than extending the current replay mechanism.
Required semantic model
The implementation should explicitly separate two modes.
Mode 1: CONNECT tunnel
One client connection == one tunnel Session is appropriate here.
Mode 2: HTTP forward proxy
The client-proxy TCP connection is a transport container, not itself the routing identity.
Whether upstream origin connections are reused is an implementation optimization and must not change request-level routing/policy semantics.
Fix strategy
Phase 1 — correctness/safety fix
If implementing a complete persistent HTTP state machine immediately is too large, prefer an explicit one-request-per-client-connection behavior for plain forward HTTP rather than the current implicit tunnel behavior.
A safe temporary strategy can be:
Simply injecting
Connection: closeinto the request without understanding the response boundary is not sufficient by itself; the implementation still needs to know when that transaction is complete before safely accepting/closing the client side.Phase 2 — persistent HTTP/1.1 forward-proxy state machine
Implement explicit request/response framing and allow persistent client connections while creating request-level routing/session semantics.
This phase should define:
Route/session/telemetry expectations
A fix must not only make the browser render correctly.
For every plain HTTP forward request, core should be able to attribute at least:
A later request must not inherit a previous request's target/route merely because the client reused the same proxy TCP connection.
CONNECT should continue to be represented as one tunnel Session.
Regression coverage
Tests should cover both
httpandmixedinbounds.Confirmed failure coverage
/firstthen/second, never an absolute URI for the second request./followed by/@vite/clientor/asset.js.mixedinbound.Routing/policy coverage
rejectcannot inherit an already-establisheddirect/proxy route from request Welcome to core Discussions! #1.Header/authority coverage
Hostis regenerated/validated consistently from the effective request target.Proxy-Connectionis not blindly forwarded to the origin.Connection-nominated hop-by-hop fields are handled correctly.Proxy-Authorizationdoes not leak to a normal origin.Transfer-Encoding+Content-Length) is rejected or normalized according to the selected HTTP parser semantics.Body/framing coverage
Content-Lengthbody.204/304no-body response.CONNECT / Upgrade coverage
Upgrade: websocketstays in HTTP mode until a successful101boundary.HTTP/Mixed parity
httpand Mixed's HTTP branch have equivalent redirect/rewrite behavior.status_code.Non-goals / do not workaround in znet-sink
Do not solve this by automatically adding DIRECT destinations to the Windows system-proxy bypass list.
These are different semantics:
Changing DIRECT into OS-level bypass would hide the HTTP proxy defect while changing routing, observability, policy, DNS/process behavior, and user expectations.
Acceptance criteria
This issue is complete when the implementation has an explicit, documented distinction between CONNECT tunnel semantics and plain HTTP forward-proxy semantics, and the following are true:
httpand Mixed's HTTP branch expose the same HTTP behavior.The immediate Vite reproduction can be used as the integration proof, but the fix should target the underlying HTTP forward-proxy semantics rather than Vite-specific behavior.