mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-03-14 12:12:40 +00:00
Compare commits
23 Commits
bo/custom_
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9fa90cefd | ||
|
|
e2bb236351 | ||
|
|
f0e26e1c35 | ||
|
|
dadaa39cce | ||
|
|
c41f278d7f | ||
|
|
889c85492a | ||
|
|
80361c71ab | ||
|
|
656443d73a | ||
|
|
8bb2bb3262 | ||
|
|
abae84e4c4 | ||
|
|
a92afb1483 | ||
|
|
b367b1ffda | ||
|
|
4b6edc091f | ||
|
|
11f3d41a67 | ||
|
|
40cb943354 | ||
|
|
1a0850d5d0 | ||
|
|
f8294c84d6 | ||
|
|
84eef6d9c1 | ||
|
|
6da3e1bae2 | ||
|
|
474eab8fb9 | ||
|
|
8b0ca9a66d | ||
|
|
bc1f81c342 | ||
|
|
2c45123de5 |
@@ -1,3 +1,4 @@
|
||||
import re
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
@@ -40,6 +41,9 @@ from onyx.utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
_TEMPLATES_DIR = Path(__file__).parent / "templates"
|
||||
_WEBAPP_HMR_FIXER_TEMPLATE = (_TEMPLATES_DIR / "webapp_hmr_fixer.js").read_text()
|
||||
|
||||
|
||||
def require_onyx_craft_enabled(user: User = Depends(current_user)) -> User:
|
||||
"""
|
||||
@@ -239,18 +243,62 @@ def _stream_response(response: httpx.Response) -> Iterator[bytes]:
|
||||
yield chunk
|
||||
|
||||
|
||||
def _inject_hmr_fixer(content: bytes, session_id: str) -> bytes:
|
||||
"""Inject a script that stubs root-scoped Next HMR websocket connections."""
|
||||
base = f"/api/build/sessions/{session_id}/webapp"
|
||||
script = f"<script>{_WEBAPP_HMR_FIXER_TEMPLATE.replace('__WEBAPP_BASE__', base)}</script>"
|
||||
text = content.decode("utf-8")
|
||||
text = re.sub(
|
||||
r"(<head\b[^>]*>)",
|
||||
lambda m: m.group(0) + script,
|
||||
text,
|
||||
count=1,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
return text.encode("utf-8")
|
||||
|
||||
|
||||
def _rewrite_asset_paths(content: bytes, session_id: str) -> bytes:
|
||||
"""Rewrite Next.js asset paths to go through the proxy."""
|
||||
import re
|
||||
|
||||
# Base path includes session_id for routing
|
||||
webapp_base_path = f"/api/build/sessions/{session_id}/webapp"
|
||||
escaped_webapp_base_path = webapp_base_path.replace("/", r"\/")
|
||||
hmr_paths = ("/_next/webpack-hmr", "/_next/hmr")
|
||||
|
||||
text = content.decode("utf-8")
|
||||
# Rewrite /_next/ paths to go through our proxy
|
||||
text = text.replace("/_next/", f"{webapp_base_path}/_next/")
|
||||
# Rewrite JSON data file fetch paths (e.g., /data.json, /data/tickets.json)
|
||||
# Matches paths like "/filename.json" or "/path/to/file.json"
|
||||
# Anchor on delimiter so already-prefixed URLs (from assetPrefix) aren't double-rewritten.
|
||||
for delim in ('"', "'", "("):
|
||||
text = text.replace(f"{delim}/_next/", f"{delim}{webapp_base_path}/_next/")
|
||||
text = re.sub(
|
||||
rf"{re.escape(delim)}https?://[^/\"')]+/_next/",
|
||||
f"{delim}{webapp_base_path}/_next/",
|
||||
text,
|
||||
)
|
||||
text = re.sub(
|
||||
rf"{re.escape(delim)}wss?://[^/\"')]+/_next/",
|
||||
f"{delim}{webapp_base_path}/_next/",
|
||||
text,
|
||||
)
|
||||
text = text.replace(r"\/_next\/", rf"{escaped_webapp_base_path}\/_next\/")
|
||||
text = re.sub(
|
||||
r"https?:\\\/\\\/[^\"']+?\\\/_next\\\/",
|
||||
rf"{escaped_webapp_base_path}\/_next\/",
|
||||
text,
|
||||
)
|
||||
text = re.sub(
|
||||
r"wss?:\\\/\\\/[^\"']+?\\\/_next\\\/",
|
||||
rf"{escaped_webapp_base_path}\/_next\/",
|
||||
text,
|
||||
)
|
||||
for hmr_path in hmr_paths:
|
||||
escaped_hmr_path = hmr_path.replace("/", r"\/")
|
||||
text = text.replace(
|
||||
f"{webapp_base_path}{hmr_path}",
|
||||
hmr_path,
|
||||
)
|
||||
text = text.replace(
|
||||
f"{escaped_webapp_base_path}{escaped_hmr_path}",
|
||||
escaped_hmr_path,
|
||||
)
|
||||
text = re.sub(
|
||||
r'"(/(?:[a-zA-Z0-9_-]+/)*[a-zA-Z0-9_-]+\.json)"',
|
||||
f'"{webapp_base_path}\\1"',
|
||||
@@ -261,11 +309,29 @@ def _rewrite_asset_paths(content: bytes, session_id: str) -> bytes:
|
||||
f"'{webapp_base_path}\\1'",
|
||||
text,
|
||||
)
|
||||
# Rewrite favicon
|
||||
text = text.replace('"/favicon.ico', f'"{webapp_base_path}/favicon.ico')
|
||||
return text.encode("utf-8")
|
||||
|
||||
|
||||
def _rewrite_proxy_response_headers(
|
||||
headers: dict[str, str], session_id: str
|
||||
) -> dict[str, str]:
|
||||
"""Rewrite response headers that can leak root-scoped asset URLs."""
|
||||
link = headers.get("link")
|
||||
if link:
|
||||
webapp_base_path = f"/api/build/sessions/{session_id}/webapp"
|
||||
rewritten_link = re.sub(
|
||||
r"<https?://[^>]+/_next/",
|
||||
f"<{webapp_base_path}/_next/",
|
||||
link,
|
||||
)
|
||||
rewritten_link = rewritten_link.replace(
|
||||
"</_next/", f"<{webapp_base_path}/_next/"
|
||||
)
|
||||
headers["link"] = rewritten_link
|
||||
return headers
|
||||
|
||||
|
||||
# Content types that may contain asset path references that need rewriting
|
||||
REWRITABLE_CONTENT_TYPES = {
|
||||
"text/html",
|
||||
@@ -342,12 +408,17 @@ def _proxy_request(
|
||||
for key, value in response.headers.items()
|
||||
if key.lower() not in EXCLUDED_HEADERS
|
||||
}
|
||||
response_headers = _rewrite_proxy_response_headers(
|
||||
response_headers, str(session_id)
|
||||
)
|
||||
|
||||
content_type = response.headers.get("content-type", "")
|
||||
|
||||
# For HTML/CSS/JS responses, rewrite asset paths
|
||||
if any(ct in content_type for ct in REWRITABLE_CONTENT_TYPES):
|
||||
content = _rewrite_asset_paths(response.content, str(session_id))
|
||||
if "text/html" in content_type:
|
||||
content = _inject_hmr_fixer(content, str(session_id))
|
||||
return Response(
|
||||
content=content,
|
||||
status_code=response.status_code,
|
||||
@@ -391,7 +462,7 @@ def _check_webapp_access(
|
||||
return session
|
||||
|
||||
|
||||
_OFFLINE_HTML_PATH = Path(__file__).parent / "templates" / "webapp_offline.html"
|
||||
_OFFLINE_HTML_PATH = _TEMPLATES_DIR / "webapp_offline.html"
|
||||
|
||||
|
||||
def _offline_html_response() -> Response:
|
||||
@@ -399,6 +470,7 @@ def _offline_html_response() -> Response:
|
||||
|
||||
Design mirrors the default Craft web template (outputs/web/app/page.tsx):
|
||||
terminal window aesthetic with Minecraft-themed typing animation.
|
||||
|
||||
"""
|
||||
html = _OFFLINE_HTML_PATH.read_text()
|
||||
return Response(content=html, status_code=503, media_type="text/html")
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
(function () {
|
||||
var WEBAPP_BASE = "__WEBAPP_BASE__";
|
||||
var PROXIED_NEXT_PREFIX = WEBAPP_BASE + "/_next/";
|
||||
var PROXIED_HMR_PREFIX = WEBAPP_BASE + "/_next/webpack-hmr";
|
||||
var PROXIED_ALT_HMR_PREFIX = WEBAPP_BASE + "/_next/hmr";
|
||||
|
||||
function isHmrWebSocketUrl(url) {
|
||||
if (!url) return false;
|
||||
try {
|
||||
var parsedUrl = new URL(String(url), window.location.href);
|
||||
return (
|
||||
parsedUrl.pathname.indexOf("/_next/webpack-hmr") === 0 ||
|
||||
parsedUrl.pathname.indexOf("/_next/hmr") === 0 ||
|
||||
parsedUrl.pathname.indexOf(PROXIED_HMR_PREFIX) === 0 ||
|
||||
parsedUrl.pathname.indexOf(PROXIED_ALT_HMR_PREFIX) === 0
|
||||
);
|
||||
} catch (e) {}
|
||||
if (typeof url === "string") {
|
||||
return (
|
||||
url.indexOf("/_next/webpack-hmr") === 0 ||
|
||||
url.indexOf("/_next/hmr") === 0 ||
|
||||
url.indexOf(PROXIED_HMR_PREFIX) === 0 ||
|
||||
url.indexOf(PROXIED_ALT_HMR_PREFIX) === 0
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function rewriteNextAssetUrl(url) {
|
||||
if (!url) return url;
|
||||
try {
|
||||
var parsedUrl = new URL(String(url), window.location.href);
|
||||
if (parsedUrl.pathname.indexOf(PROXIED_NEXT_PREFIX) === 0) {
|
||||
return parsedUrl.pathname + parsedUrl.search + parsedUrl.hash;
|
||||
}
|
||||
if (parsedUrl.pathname.indexOf("/_next/") === 0) {
|
||||
return (
|
||||
WEBAPP_BASE + parsedUrl.pathname + parsedUrl.search + parsedUrl.hash
|
||||
);
|
||||
}
|
||||
} catch (e) {}
|
||||
if (typeof url === "string") {
|
||||
if (url.indexOf(PROXIED_NEXT_PREFIX) === 0) {
|
||||
return url;
|
||||
}
|
||||
if (url.indexOf("/_next/") === 0) {
|
||||
return WEBAPP_BASE + url;
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function createEvent(eventType) {
|
||||
return typeof Event === "function"
|
||||
? new Event(eventType)
|
||||
: { type: eventType };
|
||||
}
|
||||
|
||||
function MockHmrWebSocket(url) {
|
||||
this.url = String(url);
|
||||
this.readyState = 1;
|
||||
this.bufferedAmount = 0;
|
||||
this.extensions = "";
|
||||
this.protocol = "";
|
||||
this.binaryType = "blob";
|
||||
this.onopen = null;
|
||||
this.onmessage = null;
|
||||
this.onerror = null;
|
||||
this.onclose = null;
|
||||
this._l = {};
|
||||
var socket = this;
|
||||
setTimeout(function () {
|
||||
socket._d("open", createEvent("open"));
|
||||
}, 0);
|
||||
}
|
||||
|
||||
MockHmrWebSocket.CONNECTING = 0;
|
||||
MockHmrWebSocket.OPEN = 1;
|
||||
MockHmrWebSocket.CLOSING = 2;
|
||||
MockHmrWebSocket.CLOSED = 3;
|
||||
|
||||
MockHmrWebSocket.prototype.addEventListener = function (eventType, callback) {
|
||||
(this._l[eventType] || (this._l[eventType] = [])).push(callback);
|
||||
};
|
||||
|
||||
MockHmrWebSocket.prototype.removeEventListener = function (
|
||||
eventType,
|
||||
callback,
|
||||
) {
|
||||
var listeners = this._l[eventType] || [];
|
||||
this._l[eventType] = listeners.filter(function (listener) {
|
||||
return listener !== callback;
|
||||
});
|
||||
};
|
||||
|
||||
MockHmrWebSocket.prototype._d = function (eventType, eventValue) {
|
||||
var listeners = this._l[eventType] || [];
|
||||
for (var i = 0; i < listeners.length; i++) {
|
||||
listeners[i].call(this, eventValue);
|
||||
}
|
||||
var handler = this["on" + eventType];
|
||||
if (typeof handler === "function") {
|
||||
handler.call(this, eventValue);
|
||||
}
|
||||
};
|
||||
|
||||
MockHmrWebSocket.prototype.send = function () {};
|
||||
|
||||
MockHmrWebSocket.prototype.close = function (code, reason) {
|
||||
if (this.readyState >= 2) return;
|
||||
this.readyState = 3;
|
||||
var closeEvent = createEvent("close");
|
||||
closeEvent.code = code === undefined ? 1000 : code;
|
||||
closeEvent.reason = reason || "";
|
||||
closeEvent.wasClean = true;
|
||||
this._d("close", closeEvent);
|
||||
};
|
||||
|
||||
if (window.WebSocket) {
|
||||
var OriginalWebSocket = window.WebSocket;
|
||||
window.WebSocket = function (url, protocols) {
|
||||
if (isHmrWebSocketUrl(url)) {
|
||||
return new MockHmrWebSocket(rewriteNextAssetUrl(url));
|
||||
}
|
||||
return protocols === undefined
|
||||
? new OriginalWebSocket(url)
|
||||
: new OriginalWebSocket(url, protocols);
|
||||
};
|
||||
window.WebSocket.prototype = OriginalWebSocket.prototype;
|
||||
Object.setPrototypeOf(window.WebSocket, OriginalWebSocket);
|
||||
["CONNECTING", "OPEN", "CLOSING", "CLOSED"].forEach(function (stateKey) {
|
||||
window.WebSocket[stateKey] = OriginalWebSocket[stateKey];
|
||||
});
|
||||
}
|
||||
})();
|
||||
256
backend/tests/unit/build/test_rewrite_asset_paths.py
Normal file
256
backend/tests/unit/build/test_rewrite_asset_paths.py
Normal file
@@ -0,0 +1,256 @@
|
||||
"""Unit tests for webapp proxy path rewriting/injection."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
from typing import Literal
|
||||
from uuid import UUID
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from fastapi import Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from onyx.server.features.build.api import api
|
||||
from onyx.server.features.build.api.api import _inject_hmr_fixer
|
||||
from onyx.server.features.build.api.api import _rewrite_asset_paths
|
||||
from onyx.server.features.build.api.api import _rewrite_proxy_response_headers
|
||||
|
||||
SESSION_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
||||
BASE = f"/api/build/sessions/{SESSION_ID}/webapp"
|
||||
|
||||
|
||||
def rewrite(html: str) -> str:
|
||||
return _rewrite_asset_paths(html.encode(), SESSION_ID).decode()
|
||||
|
||||
|
||||
def inject(html: str) -> str:
|
||||
return _inject_hmr_fixer(html.encode(), SESSION_ID).decode()
|
||||
|
||||
|
||||
class TestNextjsPathRewriting:
|
||||
def test_rewrites_bare_next_script_src(self) -> None:
|
||||
html = '<script src="/_next/static/chunks/main.js">'
|
||||
result = rewrite(html)
|
||||
assert f'src="{BASE}/_next/static/chunks/main.js"' in result
|
||||
assert '"/_next/' not in result
|
||||
|
||||
def test_rewrites_bare_next_in_single_quotes(self) -> None:
|
||||
html = "<link href='/_next/static/css/app.css'>"
|
||||
result = rewrite(html)
|
||||
assert f"'{BASE}/_next/static/css/app.css'" in result
|
||||
|
||||
def test_rewrites_bare_next_in_url_parens(self) -> None:
|
||||
html = "background: url(/_next/static/media/font.woff2)"
|
||||
result = rewrite(html)
|
||||
assert f"url({BASE}/_next/static/media/font.woff2)" in result
|
||||
|
||||
def test_no_double_prefix_when_already_proxied(self) -> None:
|
||||
"""assetPrefix makes Next.js emit already-prefixed URLs — must not double-rewrite."""
|
||||
already_prefixed = f'<script src="{BASE}/_next/static/chunks/main.js">'
|
||||
result = rewrite(already_prefixed)
|
||||
# Should be unchanged
|
||||
assert result == already_prefixed
|
||||
# Specifically, no double path
|
||||
assert f"{BASE}/{BASE}" not in result
|
||||
|
||||
def test_rewrites_favicon(self) -> None:
|
||||
html = '<link rel="icon" href="/favicon.ico">'
|
||||
result = rewrite(html)
|
||||
assert f'"{BASE}/favicon.ico"' in result
|
||||
|
||||
def test_rewrites_json_data_path_double_quoted(self) -> None:
|
||||
html = 'fetch("/data/tickets.json")'
|
||||
result = rewrite(html)
|
||||
assert f'"{BASE}/data/tickets.json"' in result
|
||||
|
||||
def test_rewrites_json_data_path_single_quoted(self) -> None:
|
||||
html = "fetch('/data/items.json')"
|
||||
result = rewrite(html)
|
||||
assert f"'{BASE}/data/items.json'" in result
|
||||
|
||||
def test_rewrites_escaped_next_font_path_in_json_script(self) -> None:
|
||||
"""Next dev can embed font asset paths in JSON-escaped script payloads."""
|
||||
html = r'{"src":"\/_next\/static\/media\/font.woff2"}'
|
||||
result = rewrite(html)
|
||||
assert (
|
||||
r'{"src":"\/api\/build\/sessions\/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\/webapp\/_next\/static\/media\/font.woff2"}'
|
||||
in result
|
||||
)
|
||||
|
||||
def test_rewrites_escaped_next_font_path_in_style_payload(self) -> None:
|
||||
"""Keep dynamically generated next/font URLs inside the session proxy."""
|
||||
html = r'{"css":"@font-face{src:url(\"\/_next\/static\/media\/font.woff2\")"}'
|
||||
result = rewrite(html)
|
||||
assert (
|
||||
r"\/api\/build\/sessions\/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\/webapp\/_next\/static\/media\/font.woff2"
|
||||
in result
|
||||
)
|
||||
|
||||
def test_rewrites_absolute_next_font_url(self) -> None:
|
||||
html = (
|
||||
'<link rel="preload" as="font" '
|
||||
'href="https://craft-dev.onyx.app/_next/static/media/font.woff2">'
|
||||
)
|
||||
result = rewrite(html)
|
||||
assert f'"{BASE}/_next/static/media/font.woff2"' in result
|
||||
|
||||
def test_rewrites_root_hmr_path(self) -> None:
|
||||
html = 'new WebSocket("wss://craft-dev.onyx.app/_next/webpack-hmr?id=abc")'
|
||||
result = rewrite(html)
|
||||
assert '"wss://craft-dev.onyx.app/_next/webpack-hmr?id=abc"' not in result
|
||||
assert '"/_next/webpack-hmr?id=abc"' in result
|
||||
|
||||
def test_rewrites_escaped_absolute_next_font_url(self) -> None:
|
||||
html = (
|
||||
r'{"href":"https:\/\/craft-dev.onyx.app\/_next\/static\/media\/font.woff2"}'
|
||||
)
|
||||
result = rewrite(html)
|
||||
assert (
|
||||
r'{"href":"\/api\/build\/sessions\/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\/webapp\/_next\/static\/media\/font.woff2"}'
|
||||
in result
|
||||
)
|
||||
|
||||
|
||||
class TestRuntimeFixerInjection:
|
||||
def test_injects_websocket_rewrite_shim(self) -> None:
|
||||
html = "<html><head></head><body></body></html>"
|
||||
result = inject(html)
|
||||
assert "window.WebSocket = function (url, protocols)" in result
|
||||
assert f'var WEBAPP_BASE = "{BASE}"' in result
|
||||
|
||||
def test_injects_hmr_websocket_stub(self) -> None:
|
||||
html = "<html><head></head><body></body></html>"
|
||||
result = inject(html)
|
||||
assert "function MockHmrWebSocket(url)" in result
|
||||
assert "return new MockHmrWebSocket(rewriteNextAssetUrl(url));" in result
|
||||
|
||||
def test_injects_before_head_contents(self) -> None:
|
||||
html = "<html><head><title>x</title></head><body></body></html>"
|
||||
result = inject(html)
|
||||
assert result.index(
|
||||
"window.WebSocket = function (url, protocols)"
|
||||
) < result.index("<title>x</title>")
|
||||
|
||||
def test_rewritten_hmr_url_still_matches_shim_intercept_logic(self) -> None:
|
||||
html = (
|
||||
"<html><head></head><body>"
|
||||
'new WebSocket("wss://craft-dev.onyx.app/_next/webpack-hmr?id=abc")'
|
||||
"</body></html>"
|
||||
)
|
||||
|
||||
rewritten = rewrite(html)
|
||||
assert '"wss://craft-dev.onyx.app/_next/webpack-hmr?id=abc"' not in rewritten
|
||||
assert 'new WebSocket("/_next/webpack-hmr?id=abc")' in rewritten
|
||||
|
||||
injected = inject(rewritten)
|
||||
|
||||
assert 'new WebSocket("/_next/webpack-hmr?id=abc")' in injected
|
||||
assert 'parsedUrl.pathname.indexOf("/_next/webpack-hmr") === 0' in injected
|
||||
|
||||
|
||||
class TestProxyHeaderRewriting:
|
||||
def test_rewrites_link_header_font_preload_paths(self) -> None:
|
||||
headers = {
|
||||
"link": (
|
||||
'</_next/static/media/font.woff2>; rel=preload; as="font"; crossorigin, '
|
||||
'</_next/static/media/font2.woff2>; rel=preload; as="font"; crossorigin'
|
||||
)
|
||||
}
|
||||
|
||||
result = _rewrite_proxy_response_headers(headers, SESSION_ID)
|
||||
|
||||
assert f"<{BASE}/_next/static/media/font.woff2>" in result["link"]
|
||||
|
||||
|
||||
class TestProxyRequestWiring:
|
||||
def test_proxy_request_rewrites_link_header_on_html_response(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
html = b"<html><head></head><body>ok</body></html>"
|
||||
upstream = httpx.Response(
|
||||
200,
|
||||
headers={
|
||||
"content-type": "text/html; charset=utf-8",
|
||||
"link": '</_next/static/media/font.woff2>; rel=preload; as="font"',
|
||||
},
|
||||
content=html,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(api, "_get_sandbox_url", lambda *_args: "http://sandbox")
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self, *_args: object, **_kwargs: object) -> None:
|
||||
pass
|
||||
|
||||
def __enter__(self) -> "FakeClient":
|
||||
return self
|
||||
|
||||
def __exit__(self, *_args: object) -> Literal[False]:
|
||||
return False
|
||||
|
||||
def get(self, _url: str, headers: dict[str, str]) -> httpx.Response:
|
||||
assert "host" not in {key.lower() for key in headers}
|
||||
return upstream
|
||||
|
||||
monkeypatch.setattr(api.httpx, "Client", FakeClient)
|
||||
|
||||
request = cast(Request, SimpleNamespace(headers={}, query_params=""))
|
||||
|
||||
response = api._proxy_request(
|
||||
"", request, UUID(SESSION_ID), cast(Session, SimpleNamespace())
|
||||
)
|
||||
|
||||
assert response.headers["link"] == (
|
||||
f'<{BASE}/_next/static/media/font.woff2>; rel=preload; as="font"'
|
||||
)
|
||||
|
||||
def test_proxy_request_injects_hmr_fixer_for_html_response(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
upstream = httpx.Response(
|
||||
200,
|
||||
headers={"content-type": "text/html; charset=utf-8"},
|
||||
content=b"<html><head><title>x</title></head><body></body></html>",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(api, "_get_sandbox_url", lambda *_args: "http://sandbox")
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self, *_args: object, **_kwargs: object) -> None:
|
||||
pass
|
||||
|
||||
def __enter__(self) -> "FakeClient":
|
||||
return self
|
||||
|
||||
def __exit__(self, *_args: object) -> Literal[False]:
|
||||
return False
|
||||
|
||||
def get(self, _url: str, headers: dict[str, str]) -> httpx.Response:
|
||||
assert "host" not in {key.lower() for key in headers}
|
||||
return upstream
|
||||
|
||||
monkeypatch.setattr(api.httpx, "Client", FakeClient)
|
||||
|
||||
request = cast(Request, SimpleNamespace(headers={}, query_params=""))
|
||||
|
||||
response = api._proxy_request(
|
||||
"", request, UUID(SESSION_ID), cast(Session, SimpleNamespace())
|
||||
)
|
||||
body = cast(bytes, response.body).decode("utf-8")
|
||||
|
||||
assert "window.WebSocket = function (url, protocols)" in body
|
||||
assert body.index("window.WebSocket = function (url, protocols)") < body.index(
|
||||
"<title>x</title>"
|
||||
)
|
||||
|
||||
def test_rewrites_absolute_link_header_font_preload_paths(self) -> None:
|
||||
headers = {
|
||||
"link": (
|
||||
"<https://craft-dev.onyx.app/_next/static/media/font.woff2>; "
|
||||
'rel=preload; as="font"; crossorigin'
|
||||
)
|
||||
}
|
||||
|
||||
result = _rewrite_proxy_response_headers(headers, SESSION_ID)
|
||||
|
||||
assert f"<{BASE}/_next/static/media/font.woff2>" in result["link"]
|
||||
Reference in New Issue
Block a user