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);
}
}
}