T4: nothing answers 500
Build / Build (push) Successful in 2m24s
Deploy / privapub.thepra.dev (push) Successful in 2m39s

NeverFiveHundredTests walks every endpoint the server maps and fills each route parameter
with junk ('x', zeroed and random ObjectIds, a dot-dot segment, 5000 characters). It calls
each one anonymously, as a persona, and with a junk token, and asserts that nothing
answers 5xx or throws, and that every /api error carries a Mastodon error body.

It found two bugs:
- A junk 'Authorization: Bearer' on any non-/api route, anonymous ones included
  (/build.json, /peasants/*, inboxes), broke the response. JwtEvents.AuthenticationFailed
  wrote a body during authentication and the endpoint then wrote its own. The 401 body now
  comes from the Challenge event, which runs only when an endpoint needs a user.
- /api answered 401, 404 and 429 with no body. UseMastodonErrorBodies gives any /api error
  that leaves without a body Mastodon's {"error": ...}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-03 10:46:20 +02:00
1 parent 5e34517e73
commit 125a49a1a0
5 files changed
+168 -4

No files matched your search

@@ -0,0 +1,29 @@
using System.Text.Json;
namespace PrivaPub.Api.Mastodon.Infrastructure
{
public static class MastodonErrors
{
public static IApplicationBuilder UseMastodonErrorBodies(this IApplicationBuilder app) =>
app.UseWhen(context => context.Request.Path.StartsWithSegments("/api"), api => api.UseStatusCodePages(async pages =>
{
var response = pages.HttpContext.Response;
response.ContentType = "application/json; charset=utf-8";
await response.WriteAsync(JsonSerializer.Serialize(new { error = Message(response.StatusCode) }, MastodonJson.Options));
}));
public static string Message(int status) => status switch
{
StatusCodes.Status401Unauthorized => "The access token is invalid",
StatusCodes.Status403Forbidden => "This action is outside the authorized scopes",
StatusCodes.Status404NotFound => "Record not found",
StatusCodes.Status405MethodNotAllowed => "Method not allowed",
StatusCodes.Status413PayloadTooLarge => "Payload too large",
StatusCodes.Status415UnsupportedMediaType => "Unsupported media type",
StatusCodes.Status422UnprocessableEntity => "Validation failed",
StatusCodes.Status429TooManyRequests => "Too many requests",
>= 500 => "Something went wrong.",
_ => "Bad request"
};
}
}
+2
View File
@@ -12,6 +12,7 @@ using Serilog;
using PrivaPub.Data;
using PrivaPub.Extensions;
using PrivaPub.Api.Mastodon.Auth;
using PrivaPub.Api.Mastodon.Infrastructure;
using PrivaPub.Infrastructure;
using PrivaPub.Infrastructure.Cli;
using PrivaPub.Infrastructure.Data;
@@ -128,6 +129,7 @@ try
app.UseHttpsRedirection();
app.UseCors("DefaultCORS");
app.UseMastodonErrorBodies();
app.UseStaticFiles();
var mediaRoot = app.Services.GetRequiredService<PrivaPub.Domain.Media.IMediaService>().Root;
+6 -3
View File
@@ -14,8 +14,11 @@ namespace PrivaPub.Services
ILogger<JwtEvents> _logger { get; set; }
const string contentType = "application/json";
public override async Task AuthenticationFailed(AuthenticationFailedContext context)
public override async Task Challenge(JwtBearerChallengeContext context)
{
context.HandleResponse();
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.Headers.WWWAuthenticate = context.AuthenticateFailure == default ? "Bearer" : "Bearer error=\"invalid_token\"";
try
{
var localizer = context.HttpContext.RequestServices.GetRequiredService<IStringLocalizer<GenericRes>>();
@@ -26,7 +29,7 @@ namespace PrivaPub.Services
catch (Exception ex)
{
_logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<JwtEvents>>();
_logger.LogError(ex, "Error at AuthenticationFailed()");
_logger.LogError(ex, "Error at Challenge()");
}
}
@@ -42,7 +45,7 @@ namespace PrivaPub.Services
catch (Exception ex)
{
_logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<JwtEvents>>();
_logger.LogError(ex, "Error at AuthenticationFailed()");
_logger.LogError(ex, "Error at Forbidden()");
}
}
}