WebView2 bypass перехватчик¶
WebView2 (Chromium внутри Miami Graphics WPF) делает HTTP запросы через WinHTTP — это API Windows, не через .NET HttpClient. Это значит наш FragmentingHttpHandler с TLS-фрагментацией к WebView2 не применяется.
Для РФ юзеров без этого превью-картинки в UI просто не грузились (Cloudflare блок по SNI на уровне сети).
Решение¶
WebView2 имеет API CoreWebView2.WebResourceRequested — event который срабатывает на каждый outbound HTTP/HTTPS запрос. Можно перехватить, обработать сами, вернуть response.
public sealed class WebView2BypassInterceptor
{
private readonly CoreWebView2 _webView;
private readonly AssetCache _cache;
private readonly HttpClient _http; // с FragmentingHttpHandler
public void Register()
{
_webView.AddWebResourceRequestedFilter("https://cdn.miamigraphicsstorage.uk/*", CoreWebView2WebResourceContext.All);
_webView.AddWebResourceRequestedFilter("https://miamigraphicsstorage.uk/*", CoreWebView2WebResourceContext.All);
_webView.WebResourceRequested += OnWebResourceRequested;
}
private async void OnWebResourceRequested(object? sender, CoreWebView2WebResourceRequestedEventArgs e)
{
var deferral = e.GetDeferral();
try
{
var url = e.Request.Uri;
// 1. Cache hit?
var cached = _cache.TryGet(url);
if (cached is not null)
{
e.Response = _webView.Environment.CreateWebResourceResponse(
new MemoryStream(cached.Value.Body),
200, "OK",
$"Content-Type: {cached.Value.ContentType}\r\nAccess-Control-Allow-Origin: *");
return;
}
// 2. Качаем через наш HttpClient с TLS fragmentation
using var req = new HttpRequestMessage(HttpMethod.Get, url);
using var resp = await _http.SendAsync(req);
var bytes = await resp.Content.ReadAsByteArrayAsync();
var contentType = resp.Content.Headers.ContentType?.ToString() ?? "application/octet-stream";
// 3. Сохраняем в кеш
_cache.Put(url, bytes, contentType);
// 4. Отдаём WebView2 как WebResourceResponse
e.Response = _webView.Environment.CreateWebResourceResponse(
new MemoryStream(bytes),
(int)resp.StatusCode, resp.ReasonPhrase ?? "OK",
$"Content-Type: {contentType}\r\nAccess-Control-Allow-Origin: *");
}
catch (Exception ex)
{
Debug.WriteLine($"[wv2-bypass] failed {e.Request.Uri}: {ex.Message}");
// Не устанавливаем e.Response → WebView2 сделает обычный запрос (упадёт, но это его проблема)
}
finally
{
deferral.Complete();
}
}
}
MemoryStream-buffered, не Response stream¶
CreateWebResourceResponse принимает Stream. Можно было бы передать await resp.Content.ReadAsStreamAsync() напрямую — это стримит в WebView2 без full buffer. Гораздо эффективнее по памяти для крупных файлов.
Но был баг: WebView2 на больших ответах (>10 МБ) иногда отрезает половину. Поток рассинхронизируется — WebView2 ждёт «больше байтов», наш stream уже закрылся.
Решение — буферить полностью в memory через MemoryStream. Чуть больше памяти на пик (50-100 МБ для большой текстуры), но надёжно. Все равно картинки и .glb редко выше 5 МБ.
Регистрация filter'а¶
AddWebResourceRequestedFilter принимает URL pattern. Без фильтра мы получим event на каждый запрос из WebView2 (включая локальные app:// и chrome://). Это медленно.
Регистрируем только наши домены:
_webView.AddWebResourceRequestedFilter(
"https://cdn.miamigraphicsstorage.uk/*",
CoreWebView2WebResourceContext.All);
_webView.AddWebResourceRequestedFilter(
"https://miamigraphicsstorage.uk/*",
CoreWebView2WebResourceContext.All);
CoreWebView2WebResourceContext.All — перехватываем все типы (image, fetch, xhr, document). Без All некоторые типы пропускаются и идут через WinHTTP.
CORS¶
В response headers мы добавляем Access-Control-Allow-Origin: *. WebView2 enforces CORS, и если React попробует fetch() на CDN-домен без правильных CORS-headers — отказ.
Изначально на R2 headers не было (по умолчанию). Добавляли в R2 bucket-CORS config, но это применяется only для прямых запросов из браузера, а наш interceptor стоит между WebView2 и R2, и мы сами формируем headers. Можем добавить любые что нужны.
Кэширование через AssetCache¶
Все ответы сохраняются в AssetCache. При повторном запросе той же картинки (юзер прокрутил грид модов 3 раза) — мгновенный hit с диска, без сети.
Это критично для UX:
- Первый load каталога: 30-60 сек (качаем 200+ превью с CDN).
- Второй load (юзер закрыл/открыл лаунчер): 1-2 сек (всё с диска).
Failure mode¶
Если interceptor упал по любой причине (HttpClient бросил, AssetCache.Put failed):
catch (Exception ex)
{
Debug.WriteLine($"[wv2-bypass] failed {e.Request.Uri}: {ex.Message}");
// Не устанавливаем e.Response → WebView2 сделает обычный запрос
}
finally { deferral.Complete(); }
Не устанавливая e.Response, WebView2 возвращается к нативному WinHTTP. Это значит для РФ юзера запрос упадёт (CF блокировка), но остальные запросы продолжат работать.
deferral.Complete() в finally — критически важно. Без него WebView2 будет висеть на этом запросе (ждёт нашего ответа), и UI зависнет. using var deferral = e.GetDeferral(); тоже бы работал, но finally более explicit.