House rules: the build checks them, and every refusal from the server answers 400, 403 or 404
Build / Desktop, server and tools (push) Successful in 1m8s

The server no longer answers 409, 413 or 429: a busy probe, a share or upload that is too large and a request over
the limit now answer 400 with the same reason as before, and still say when to try again. The app reads the reason,
so nothing changes for it; versions up to 0.15.4 report a limited speed test as unmeasured until they update.
The app's commands can now be cancelled all the way down, and the house rules are written into PATTERNS.md and
checked on every build, Android included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4YtEJrgxG12Pc39Fai8Bd
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-11-10 05:32:04 +01:00
1 parent 5b2210a8a7
commit d408edea98
28 files changed
+235 -145

No files matched your search

+12 -11
View File
@@ -1,4 +1,5 @@
using System.Net.Sockets;
using System.Text.Json.Serialization.Metadata;
using MeWeb.Core.Knowledge;
using MeWeb.Shared;
@@ -46,8 +47,8 @@ public static class ServerApi
{
var caller = Caller(context);
if (caller is null) return Results.BadRequest();
if (!speed.Allow(caller)) return Refused(context, new ViewRefusal { Reason = "too-many", RetryAfterSeconds = 3600 }, StatusCodes.Status429TooManyRequests);
if (!speed.Enter()) return Refused(context, new ViewRefusal { Reason = "busy", RetryAfterSeconds = 60 }, StatusCodes.Status429TooManyRequests);
if (!speed.Allow(caller)) return Refused(context, new ViewRefusal { Reason = "too-many", RetryAfterSeconds = 3600 }, StatusCodes.Status400BadRequest);
if (!speed.Enter()) return Refused(context, new ViewRefusal { Reason = "busy", RetryAfterSeconds = 60 }, StatusCodes.Status400BadRequest);
try
{
var size = Math.Clamp(bytes ?? 1024 * 1024, 1024, Endpoints.Speed.MaxDownBytes);
@@ -63,7 +64,7 @@ public static class ServerApi
static async Task<IResult> Up(HttpContext context, CancellationToken ct)
{
var received = await SpeedService.Drain(context.Request.Body, ct);
return received < 0 ? Results.StatusCode(StatusCodes.Status413PayloadTooLarge) : Results.NoContent();
return received < 0 ? Refused(context, new ViewRefusal { Reason = "size" }, StatusCodes.Status400BadRequest) : Results.NoContent();
}
public const string DeleteTokenHeader = "X-Delete-Token";
@@ -72,16 +73,14 @@ public static class ServerApi
{
var caller = Caller(context);
if (caller is null) return Results.BadRequest();
var (ticket, refusal, status) = probe.Issue(caller);
return ticket is not null ? Results.Json(ticket, MeWebJsonContext.Default.ViewProbeTicket) : Refused(context, refusal, status);
return Answer(context, probe.Issue(caller), MeWebJsonContext.Default.ViewProbeTicket);
}
static async Task<IResult> Run(string id, ProbeForm form, HttpContext context, ProbeService probe, CancellationToken ct)
{
var caller = Caller(context);
if (caller is null) return Results.BadRequest();
var (result, refusal, status) = await probe.Run(id, caller, form, ct);
return result is not null ? Results.Json(result, MeWebJsonContext.Default.ViewProbe) : Refused(context, refusal, status);
return Answer(context, await probe.Run(id, caller, form, ct), MeWebJsonContext.Default.ViewProbe);
}
static async Task<IResult> Share(HttpContext context, ShareService shares, CancellationToken ct)
@@ -90,19 +89,21 @@ public static class ServerApi
if (caller is null) return Results.BadRequest();
using var buffer = new MemoryStream();
var chunk = new byte[8192];
int read;
var read = default(int);
while ((read = await context.Request.Body.ReadAsync(chunk, ct)) > 0)
{
buffer.Write(chunk, 0, read);
if (buffer.Length > ShareService.MaxBytes) return Refused(context, new ViewRefusal { Reason = "size" }, StatusCodes.Status413PayloadTooLarge);
if (buffer.Length > ShareService.MaxBytes) return Refused(context, new ViewRefusal { Reason = "size" }, StatusCodes.Status400BadRequest);
}
var (shared, refusal, status) = shares.Create(caller, buffer.ToArray());
return shared is not null ? Results.Json(shared, MeWebJsonContext.Default.ViewShared) : Refused(context, refusal, status);
return Answer(context, shares.Create(caller, buffer.ToArray()), MeWebJsonContext.Default.ViewShared);
}
static IResult Unshare(string id, HttpContext context, ShareService shares) =>
Results.StatusCode(shares.Delete(id, context.Request.Headers[DeleteTokenHeader].ToString()));
static IResult Answer<T>(HttpContext context, WebResult<T> result, JsonTypeInfo<T> type) where T : class =>
result.IsInvalid ? Refused(context, result.Refusal, result.StatusCode) : Results.Json(result.Data, type);
static IResult Refused(HttpContext context, ViewRefusal refusal, int status)
{
if (refusal.RetryAfterSeconds > 0) context.Response.Headers.RetryAfter = refusal.RetryAfterSeconds.ToString(System.Globalization.CultureInfo.InvariantCulture);