using System; using System.IO; using System.Text; using System.Reflection; using BestHTTP; using HarmonyLib; /// /// Fabric-level repairs for two things the game cannot survive as-is: /// a chunked response the server truncated, and two responses whose shape the /// client's reader rejects. /// internal static class HttpTolerancePatches { private static long _healed; private static int _lastHealReport; internal static void Apply(Harmony harmony) { try { var target = TargetMethod(); if (target == null) { Diag.Warn("[Http] BestHTTP.HTTPResponse.ReadChunkLength(Stream) was not found: " + "a truncated chunked body will keep failing its request."); return; } harmony.Patch(target, finalizer: new HarmonyMethod(AccessTools.Method(typeof(HttpTolerancePatches), nameof(Finalizer)))); Diag.Info("[Http] truncated chunked responses are completed instead of failing their request."); } catch (Exception ex) { Diag.Warn("[Http] could not arm the tolerant chunked reader (" + ex.GetType().Name + ": " + ex.Message + ")."); } } internal static MethodBase TargetMethod() => AccessTools.Method(typeof(HTTPResponse), "ReadChunkLength", new[] { typeof(Stream) }); /// /// "Can't parse '' as a hex number" is BestHTTP reading a chunk-size line from a /// connection the server had already closed - the pooled keep-alive connection /// it picked was dead. Ending the body there (chunk length 0) delivers the /// response that did arrive instead of failing the request, which is what the /// heartbeats need: their ack body is empty anyway. /// private static Exception Finalizer(Exception __exception, ref int __result) { try { if (__exception == null) return null; if (!ResponseRepair.IsTruncatedChunkFailure(__exception.Message)) return __exception; ReportHealed(); __result = 0; __exception = null; return null; } catch { return __exception; } } private static void ReportHealed() { var count = System.Threading.Interlocked.Increment(ref _healed); var now = Environment.TickCount; if (count != 1 && unchecked(now - _lastHealReport) < 60000) return; _lastHealReport = now; Diag.Warn("[Http] a chunked response ended early (dead pooled connection); the request was completed " + "with the bytes that arrived instead of failing. Occurrences so far: " + count + "."); } /// /// Applies to a live response before the client's own /// callback reads it. Called from the request funnel, always on the transport /// thread that owns the response. /// internal static void RepairResponse(HTTPResponse response, string path, string query) { try { if (response == null || string.IsNullOrEmpty(path)) return; if (response.StatusCode != 200) return; string body; try { body = response.DataAsText; } catch { return; } var repaired = ResponseRepair.RepairBody(path, query, body, out var note); if (repaired == null) return; response.Data = Encoding.UTF8.GetBytes(repaired); if (Diag.FirstTime("repair:" + path)) Diag.Warn("[Repair] " + path + ": " + note + "."); } catch (Exception ex) { Diag.Warn("[Repair] could not repair a " + path + " response: " + ex.GetType().Name + ": " + ex.Message); } } }