From 5d452824cdb8785434bfa7f23a6f70a17dffc911 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 11 Mar 2025 16:10:55 +0100 Subject: [PATCH 1/3] refactor(backend): use single shared HTTP client with backoff --- .../Extensions/WebApplicationExtensions.cs | 36 +++ Foxnouns.Backend/Foxnouns.Backend.csproj | 13 +- Foxnouns.Backend/Jobs/CreateDataExportJob.cs | 4 +- .../Services/Auth/FediverseAuthService.cs | 8 +- .../Auth/RemoteAuthService.Discord.cs | 4 +- .../Services/Auth/RemoteAuthService.Google.cs | 2 +- .../Services/Auth/RemoteAuthService.Tumblr.cs | 4 +- .../Services/Auth/RemoteAuthService.cs | 2 +- Foxnouns.Backend/packages.lock.json | 234 ++++++++++++++---- 9 files changed, 232 insertions(+), 75 deletions(-) diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 8db7a1b..6a010fc 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -21,8 +21,10 @@ using Foxnouns.Backend.Services; using Foxnouns.Backend.Services.Auth; using Foxnouns.Backend.Services.V1; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Http.Resilience; using Minio; using NodaTime; +using Polly; using Prometheus; using Serilog; using Serilog.Events; @@ -100,6 +102,40 @@ public static class WebApplicationExtensions builder.Host.ConfigureServices( (ctx, services) => { + // create a single HTTP client for all requests. + // it's also configured with a retry mechanism, so that requests aren't immediately lost to the void if they fail + services.AddSingleton(_ => + { + // ReSharper disable once SuggestVarOrType_Elsewhere + var retryPipeline = new ResiliencePipelineBuilder() + .AddRetry( + new HttpRetryStrategyOptions + { + BackoffType = DelayBackoffType.Linear, + MaxRetryAttempts = 3, + } + ) + .Build(); + + var resilienceHandler = new ResilienceHandler(retryPipeline) + { + InnerHandler = new SocketsHttpHandler + { + PooledConnectionLifetime = TimeSpan.FromMinutes(15), + }, + }; + + var client = new HttpClient(resilienceHandler); + client.DefaultRequestHeaders.Remove("User-Agent"); + client.DefaultRequestHeaders.Remove("Accept"); + client.DefaultRequestHeaders.Add( + "User-Agent", + $"pronouns.cc/{BuildInfo.Version}" + ); + client.DefaultRequestHeaders.Add("Accept", "application/json"); + return client; + }); + services .AddQueue() .AddSmtpMailer(ctx.Configuration) diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index a9972ab..42a3beb 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -25,12 +25,13 @@ all + - - + + @@ -38,14 +39,14 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - - + + diff --git a/Foxnouns.Backend/Jobs/CreateDataExportJob.cs b/Foxnouns.Backend/Jobs/CreateDataExportJob.cs index 3662e33..1c392f7 100644 --- a/Foxnouns.Backend/Jobs/CreateDataExportJob.cs +++ b/Foxnouns.Backend/Jobs/CreateDataExportJob.cs @@ -27,6 +27,7 @@ using NodaTime.Text; namespace Foxnouns.Backend.Jobs; public class CreateDataExportJob( + HttpClient client, DatabaseContext db, IClock clock, UserRendererService userRenderer, @@ -36,7 +37,6 @@ public class CreateDataExportJob( ILogger logger ) { - private static readonly HttpClient Client = new(); private readonly ILogger _logger = logger.ForContext(); public static void Enqueue(Snowflake userId) @@ -201,7 +201,7 @@ public class CreateDataExportJob( if (s3Path == null) return; - HttpResponseMessage resp = await Client.GetAsync(s3Path); + HttpResponseMessage resp = await client.GetAsync(s3Path); if (resp.StatusCode != HttpStatusCode.OK) { _logger.Warning("S3 path {S3Path} returned a non-200 status, not saving file", s3Path); diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs index 49afe1d..daa15ab 100644 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs +++ b/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs @@ -34,6 +34,7 @@ public partial class FediverseAuthService ILogger logger, Config config, DatabaseContext db, + HttpClient client, KeyCacheService keyCacheService, ISnowflakeGenerator snowflakeGenerator ) @@ -43,12 +44,7 @@ public partial class FediverseAuthService _db = db; _keyCacheService = keyCacheService; _snowflakeGenerator = snowflakeGenerator; - - _client = new HttpClient(); - _client.DefaultRequestHeaders.Remove("User-Agent"); - _client.DefaultRequestHeaders.Remove("Accept"); - _client.DefaultRequestHeaders.Add("User-Agent", $"pronouns.cc/{BuildInfo.Version}"); - _client.DefaultRequestHeaders.Add("Accept", "application/json"); + _client = client; } public async Task GenerateAuthUrlAsync( diff --git a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs index de16d2f..d884fda 100644 --- a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs +++ b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs @@ -27,7 +27,7 @@ public partial class RemoteAuthService ) { var redirectUri = $"{config.BaseUrl}/auth/callback/discord"; - HttpResponseMessage resp = await _httpClient.PostAsync( + HttpResponseMessage resp = await client.PostAsync( _discordTokenUri, new FormUrlEncodedContent( new Dictionary @@ -59,7 +59,7 @@ public partial class RemoteAuthService var req = new HttpRequestMessage(HttpMethod.Get, _discordUserUri); req.Headers.Add("Authorization", $"{token.TokenType} {token.AccessToken}"); - HttpResponseMessage resp2 = await _httpClient.SendAsync(req, ct); + HttpResponseMessage resp2 = await client.SendAsync(req, ct); resp2.EnsureSuccessStatusCode(); DiscordUserResponse? user = await resp2.Content.ReadFromJsonAsync(ct); if (user == null) diff --git a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Google.cs b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Google.cs index 3245858..938ba32 100644 --- a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Google.cs +++ b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Google.cs @@ -28,7 +28,7 @@ public partial class RemoteAuthService ) { var redirectUri = $"{config.BaseUrl}/auth/callback/google"; - HttpResponseMessage resp = await _httpClient.PostAsync( + HttpResponseMessage resp = await client.PostAsync( _googleTokenUri, new FormUrlEncodedContent( new Dictionary diff --git a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Tumblr.cs b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Tumblr.cs index d63ee1a..45b9161 100644 --- a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Tumblr.cs +++ b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Tumblr.cs @@ -29,7 +29,7 @@ public partial class RemoteAuthService ) { var redirectUri = $"{config.BaseUrl}/auth/callback/tumblr"; - HttpResponseMessage resp = await _httpClient.PostAsync( + HttpResponseMessage resp = await client.PostAsync( _tumblrTokenUri, new FormUrlEncodedContent( new Dictionary @@ -62,7 +62,7 @@ public partial class RemoteAuthService var req = new HttpRequestMessage(HttpMethod.Get, _tumblrUserUri); req.Headers.Add("Authorization", $"Bearer {token.AccessToken}"); - HttpResponseMessage resp2 = await _httpClient.SendAsync(req, ct); + HttpResponseMessage resp2 = await client.SendAsync(req, ct); if (!resp2.IsSuccessStatusCode) { string respBody = await resp2.Content.ReadAsStringAsync(ct); diff --git a/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs b/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs index 6e0ba76..93b006b 100644 --- a/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs +++ b/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs @@ -25,6 +25,7 @@ using Microsoft.EntityFrameworkCore; namespace Foxnouns.Backend.Services.Auth; public partial class RemoteAuthService( + HttpClient client, Config config, ILogger logger, DatabaseContext db, @@ -32,7 +33,6 @@ public partial class RemoteAuthService( ) { private readonly ILogger _logger = logger.ForContext(); - private readonly HttpClient _httpClient = new(); public record RemoteUser(string Id, string Username); diff --git a/Foxnouns.Backend/packages.lock.json b/Foxnouns.Backend/packages.lock.json index 96daa5d..365ee8c 100644 --- a/Foxnouns.Backend/packages.lock.json +++ b/Foxnouns.Backend/packages.lock.json @@ -155,6 +155,18 @@ "Microsoft.Extensions.Primitives": "9.0.2" } }, + "Microsoft.Extensions.Http.Resilience": { + "type": "Direct", + "requested": "[9.2.0, )", + "resolved": "9.2.0", + "contentHash": "Km+YyCuk1IaeOsAzPDygtgsUOh3Fi89hpA18si0tFJmpSBf9aKzP9ffV5j7YOoVDvRWirpumXAPQzk1inBsvKw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Binder": "9.0.2", + "Microsoft.Extensions.Http.Diagnostics": "9.2.0", + "Microsoft.Extensions.ObjectPool": "9.0.2", + "Microsoft.Extensions.Resilience": "9.2.0" + } + }, "MimeKit": { "type": "Direct", "requested": "[4.10.0, )", @@ -537,6 +549,16 @@ "Microsoft.Extensions.Logging": "9.0.2" } }, + "Microsoft.Extensions.AmbientMetadata.Application": { + "type": "Transitive", + "resolved": "9.2.0", + "contentHash": "GMCX3zybUB22aAADjYPXrWhhd1HNMkcY5EcFAJnXy/4k5pPpJ6TS4VRl37xfrtosNyzbpO2SI7pd2Q5PvggSdg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.2", + "Microsoft.Extensions.Hosting.Abstractions": "9.0.2", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2" + } + }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", "resolved": "9.0.2", @@ -545,13 +567,22 @@ "Microsoft.Extensions.Primitives": "9.0.2" } }, + "Microsoft.Extensions.Compliance.Abstractions": { + "type": "Transitive", + "resolved": "9.2.0", + "contentHash": "Te+N4xphDlGIS90lKJMZyezFiMWKLAtYV2/M8gGJG4thH6xyC7LWhMzgz2+tWMehxwZlBUq2D9DvVpjKBZFTPQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2", + "Microsoft.Extensions.ObjectPool": "9.0.2" + } + }, "Microsoft.Extensions.Configuration": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "YIMO9T3JL8MeEXgVozKt2v79hquo/EFtnY0vgxmLnUvk1Rei/halI7kOWZL2RBeV9FMGzgM9LZA8CVaNwFMaNA==", + "resolved": "9.0.2", + "contentHash": "EBZW+u96tApIvNtjymXEIS44tH0I/jNwABHo4c33AchWOiDWCq2rL3klpnIo+xGrxoVGJzPDISV6hZ+a9C9SzQ==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", - "Microsoft.Extensions.Primitives": "9.0.0" + "Microsoft.Extensions.Configuration.Abstractions": "9.0.2", + "Microsoft.Extensions.Primitives": "9.0.2" } }, "Microsoft.Extensions.Configuration.Abstractions": { @@ -564,10 +595,10 @@ }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "RiScL99DcyngY9zJA2ROrri7Br8tn5N4hP4YNvGdTN/bvg1A3dwvDOxHnNZ3Im7x2SJ5i4LkX1uPiR/MfSFBLQ==", + "resolved": "9.0.2", + "contentHash": "krJ04xR0aPXrOf5dkNASg6aJjsdzexvsMRL6UNOUjiTzqBvRr95sJ1owoKEm89bSONQCfZNhHrAFV9ahDqIPIw==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "9.0.0" + "Microsoft.Extensions.Configuration.Abstractions": "9.0.2" } }, "Microsoft.Extensions.DependencyInjection": { @@ -583,6 +614,14 @@ "resolved": "9.0.2", "contentHash": "MNe7GSTBf3jQx5vYrXF0NZvn6l7hUKF6J54ENfAgCO8y6xjN1XUmKKWG464LP2ye6QqDiA1dkaWEZBYnhoZzjg==" }, + "Microsoft.Extensions.DependencyInjection.AutoActivation": { + "type": "Transitive", + "resolved": "9.2.0", + "contentHash": "WcwfTpl3IcPcaahTVEaJwMUg1eWog1SkIA6jQZZFqMXiMX9/tVkhNB6yzUQmBdGWdlWDDRKpOmK7T7x1Uu05pQ==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "9.0.2" + } + }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", "resolved": "9.0.2", @@ -590,54 +629,74 @@ }, "Microsoft.Extensions.Diagnostics": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "0CF9ZrNw5RAlRfbZuVIvzzhP8QeWqHiUmMBU/2H7Nmit8/vwP3/SbHeEctth7D4Gz2fBnEbokPc1NU8/j/1ZLw==", + "resolved": "9.0.2", + "contentHash": "kwFWk6DPaj1Roc0CExRv+TTwjsiERZA730jQIPlwCcS5tMaCAQtaGfwAK0z8CMFpVTiT+MgKXpd/P50qVCuIgg==", "dependencies": { - "Microsoft.Extensions.Configuration": "9.0.0", - "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.0" + "Microsoft.Extensions.Configuration": "9.0.2", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.2", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2" } }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "1K8P7XzuzX8W8pmXcZjcrqS6x5eSSdvhQohmcpgiQNY/HlDAlnrhR9dvlURfFz428A+RTCJpUyB+aKTA6AgVcQ==", + "resolved": "9.0.2", + "contentHash": "kFwIZEC/37cwKuEm/nXvjF7A/Myz9O7c7P9Csgz6AOiiDE62zdOG5Bu7VkROu1oMYaX0wgijPJ5LqVt6+JKjVg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", - "Microsoft.Extensions.Options": "9.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2", + "Microsoft.Extensions.Options": "9.0.2" + } + }, + "Microsoft.Extensions.Diagnostics.ExceptionSummarization": { + "type": "Transitive", + "resolved": "9.2.0", + "contentHash": "et5JevHsLv1w1O1Zhb6LiUfai/nmDRzIHnbrZJdzLsIbbMCKTZpeHuANYIppAD//n12KvgOne05j4cu0GhG9gw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2" } }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "uK439QzYR0q2emLVtYzwyK3x+T5bTY4yWsd/k/ZUS9LR6Sflp8MIdhGXW8kQCd86dQD4tLqvcbLkku8qHY263Q==", + "resolved": "9.0.2", + "contentHash": "IcOBmTlr2jySswU+3x8c3ql87FRwTVPQgVKaV5AXzPT5u0VItfNU8SMbESpdSp5STwxT/1R99WYszgHWsVkzhg==", "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.0" + "Microsoft.Extensions.Primitives": "9.0.2" } }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "yUKJgu81ExjvqbNWqZKshBbLntZMbMVz/P7Way2SBx7bMqA08Mfdc9O7hWDKAiSp+zPUGT6LKcSCQIPeDK+CCw==", + "resolved": "9.0.2", + "contentHash": "PvjZW6CMdZbPbOwKsQXYN5VPtIWZQqdTRuBPZiW3skhU3hymB17XSlLVC4uaBbDZU+/3eHG3p80y+MzZxZqR7Q==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", - "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "9.0.0", - "Microsoft.Extensions.Logging.Abstractions": "9.0.0" + "Microsoft.Extensions.Configuration.Abstractions": "9.0.2", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.2", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.2", + "Microsoft.Extensions.Logging.Abstractions": "9.0.2" } }, "Microsoft.Extensions.Http": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "DqI4q54U4hH7bIAq9M5a/hl5Odr/KBAoaZ0dcT4OgutD8dook34CbkvAfAIzkMVjYXiL+E5ul9etwwqiX4PHGw==", + "resolved": "9.0.2", + "contentHash": "34+kcwxPZr3Owk9eZx268+gqGNB8G/8Y96gZHomxam0IOH08FhPBjPrLWDtKdVn4+sVUUJnJMpECSTJi4XXCcg==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", - "Microsoft.Extensions.Diagnostics": "9.0.0", - "Microsoft.Extensions.Logging": "9.0.0", - "Microsoft.Extensions.Logging.Abstractions": "9.0.0", - "Microsoft.Extensions.Options": "9.0.0" + "Microsoft.Extensions.Configuration.Abstractions": "9.0.2", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2", + "Microsoft.Extensions.Diagnostics": "9.0.2", + "Microsoft.Extensions.Logging": "9.0.2", + "Microsoft.Extensions.Logging.Abstractions": "9.0.2", + "Microsoft.Extensions.Options": "9.0.2" + } + }, + "Microsoft.Extensions.Http.Diagnostics": { + "type": "Transitive", + "resolved": "9.2.0", + "contentHash": "Eeup1LuD5hVk5SsKAuX1D7I9sF380MjrNG10IaaauRLOmrRg8rq2TA8PYTXVBXf3MLkZ6m2xpBqRbZdxf8ygkg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.AutoActivation": "9.2.0", + "Microsoft.Extensions.Http": "9.0.2", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2", + "Microsoft.Extensions.Telemetry": "9.2.0", + "System.IO.Pipelines": "9.0.2" } }, "Microsoft.Extensions.Logging": { @@ -660,23 +719,23 @@ }, "Microsoft.Extensions.Logging.Configuration": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "H05HiqaNmg6GjH34ocYE9Wm1twm3Oz2aXZko8GTwGBzM7op2brpAA8pJ5yyD1OpS1mXUtModBYOlcZ/wXeWsSg==", + "resolved": "9.0.2", + "contentHash": "pnwYZE7U6d3Y6iMVqADOAUUMMBGYAQPsT3fMwVr/V1Wdpe5DuVGFcViZavUthSJ5724NmelIl1cYy+kRfKfRPQ==", "dependencies": { - "Microsoft.Extensions.Configuration": "9.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", - "Microsoft.Extensions.Configuration.Binder": "9.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", - "Microsoft.Extensions.Logging": "9.0.0", - "Microsoft.Extensions.Logging.Abstractions": "9.0.0", - "Microsoft.Extensions.Options": "9.0.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.0" + "Microsoft.Extensions.Configuration": "9.0.2", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.2", + "Microsoft.Extensions.Configuration.Binder": "9.0.2", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2", + "Microsoft.Extensions.Logging": "9.0.2", + "Microsoft.Extensions.Logging.Abstractions": "9.0.2", + "Microsoft.Extensions.Options": "9.0.2", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2" } }, "Microsoft.Extensions.ObjectPool": { "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "udvKco0sAVgYGTBnHUb0tY9JQzJ/nPDiv/8PIyz69wl1AibeCDZOLVVI+6156dPfHmJH7ws5oUJRiW4ZmAvuuA==" + "resolved": "9.0.2", + "contentHash": "nWx7uY6lfkmtpyC2dGc0IxtrZZs/LnLCQHw3YYQucbqWj8a27U/dZ+eh72O3ZiolqLzzLkVzoC+w/M8dZwxRTw==" }, "Microsoft.Extensions.Options": { "type": "Transitive", @@ -689,14 +748,14 @@ }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "Ob3FXsXkcSMQmGZi7qP07EQ39kZpSBlTcAZLbJLdI4FIf0Jug8biv2HTavWmnTirchctPlq9bl/26CXtQRguzA==", + "resolved": "9.0.2", + "contentHash": "OPm1NXdMg4Kb4Kz+YHdbBQfekh7MqQZ7liZ5dYUd+IbJakinv9Fl7Ck6Strbgs0a6E76UGbP/jHR532K/7/feQ==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", - "Microsoft.Extensions.Configuration.Binder": "9.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", - "Microsoft.Extensions.Options": "9.0.0", - "Microsoft.Extensions.Primitives": "9.0.0" + "Microsoft.Extensions.Configuration.Abstractions": "9.0.2", + "Microsoft.Extensions.Configuration.Binder": "9.0.2", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2", + "Microsoft.Extensions.Options": "9.0.2", + "Microsoft.Extensions.Primitives": "9.0.2" } }, "Microsoft.Extensions.Primitives": { @@ -704,6 +763,42 @@ "resolved": "9.0.2", "contentHash": "puBMtKe/wLuYa7H6docBkLlfec+h8L35DXqsDKKJgW0WY5oCwJ3cBJKcDaZchv6knAyqOMfsl6VUbaR++E5LXA==" }, + "Microsoft.Extensions.Resilience": { + "type": "Transitive", + "resolved": "9.2.0", + "contentHash": "dyaM+Jeznh/i21bOrrRs3xceFfn0571EOjOq95dRXmL1rHDLC4ExhACJ2xipRBP6g1AgRNqmryi+hMrVWWgmlg==", + "dependencies": { + "Microsoft.Extensions.Diagnostics": "9.0.2", + "Microsoft.Extensions.Diagnostics.ExceptionSummarization": "9.2.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2", + "Microsoft.Extensions.Telemetry.Abstractions": "9.2.0", + "Polly.Extensions": "8.4.2", + "Polly.RateLimiting": "8.4.2" + } + }, + "Microsoft.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "9.2.0", + "contentHash": "4+bw7W4RrAMrND9TxonnSmzJOdXiPxljoda8OPJiReIN607mKCc0t0Mf28sHNsTujO1XQw28wsI0poxeeQxohw==", + "dependencies": { + "Microsoft.Extensions.AmbientMetadata.Application": "9.2.0", + "Microsoft.Extensions.DependencyInjection.AutoActivation": "9.2.0", + "Microsoft.Extensions.Logging.Configuration": "9.0.2", + "Microsoft.Extensions.ObjectPool": "9.0.2", + "Microsoft.Extensions.Telemetry.Abstractions": "9.2.0" + } + }, + "Microsoft.Extensions.Telemetry.Abstractions": { + "type": "Transitive", + "resolved": "9.2.0", + "contentHash": "kEl+5G3RqS20XaEhHh/nOugcjKEK+rgVtMJra1iuwNzdzQXElelf3vu8TugcT7rIZ/T4T76EKW1OX/fmlxz4hw==", + "dependencies": { + "Microsoft.Extensions.Compliance.Abstractions": "9.2.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.2", + "Microsoft.Extensions.ObjectPool": "9.0.2", + "Microsoft.Extensions.Options": "9.0.2" + } + }, "Microsoft.NETCore.Platforms": { "type": "Transitive", "resolved": "1.1.1", @@ -760,6 +855,30 @@ "System.IO.Pipelines": "5.0.1" } }, + "Polly.Core": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" + }, + "Polly.Extensions": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "GZ9vRVmR0jV2JtZavt+pGUsQ1O1cuRKG7R7VOZI6ZDy9y6RNPvRvXK1tuS4ffUrv8L0FTea59oEuQzgS0R7zSA==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Polly.Core": "8.4.2" + } + }, + "Polly.RateLimiting": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "ehTImQ/eUyO07VYW2WvwSmU9rRH200SKJ/3jku9rOkyWE0A2JxNFmAVms8dSn49QLSjmjFRRSgfNyOgr/2PSmA==", + "dependencies": { + "Polly.Core": "8.4.2", + "System.Threading.RateLimiting": "8.0.0" + } + }, "Sentry": { "type": "Transitive", "resolved": "5.3.0", @@ -901,8 +1020,8 @@ }, "System.IO.Pipelines": { "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "jRn6JYnNPW6xgQazROBLSfpdoczRw694vO5kKvMcNnpXuolEixUyw6IBuBs2Y2mlSX/LdLvyyWmfXhaI3ND1Yg==" + "resolved": "9.0.2", + "contentHash": "UIBaK7c/A3FyQxmX/747xw4rCUkm1BhNiVU617U5jweNJssNjLJkPUGhBsrlDG0BpKWCYKsncD+Kqpy4KmvZZQ==" }, "System.Reactive": { "type": "Transitive", @@ -940,6 +1059,11 @@ "type": "Transitive", "resolved": "7.0.0", "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==" + }, + "System.Threading.RateLimiting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" } } } From f5f04163463ca4a5274bfb359991164eaccc9b19 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 13 Mar 2025 15:18:35 +0100 Subject: [PATCH 2/3] refactor(backend): misc cleanup --- .../Extensions/WebApplicationExtensions.cs | 3 -- Foxnouns.Backend/Program.cs | 11 ++--- .../Auth/FediverseAuthService.Mastodon.cs | 24 +++++------ .../Auth/FediverseAuthService.Misskey.cs | 18 ++++----- .../Services/Auth/FediverseAuthService.cs | 40 ++++++------------- Foxnouns.Backend/Services/KeyCacheService.cs | 2 - .../Services/PeriodicTasksService.cs | 2 - .../Services/ValidationService.Strings.cs | 5 ++- 8 files changed, 43 insertions(+), 62 deletions(-) diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 6a010fc..567ae02 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -196,9 +196,6 @@ public static class WebApplicationExtensions public static async Task Initialize(this WebApplication app, string[] args) { - // Read version information from .version in the repository root - await BuildInfo.ReadBuildInfo(); - app.Services.ConfigureQueue() .LogQueuedTaskProgress(app.Services.GetRequiredService>()); diff --git a/Foxnouns.Backend/Program.cs b/Foxnouns.Backend/Program.cs index b5bc338..f27ad50 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -34,6 +34,9 @@ Config config = builder.AddConfiguration(); builder.AddSerilog(); +// Read version information from .version in the repository root +await BuildInfo.ReadBuildInfo(); + builder .WebHost.UseSentry(opts => { @@ -68,11 +71,9 @@ builder }) .ConfigureApiBehaviorOptions(options => { - // the type isn't needed but without it, rider keeps complaining for no reason (it compiles just fine) - options.InvalidModelStateResponseFactory = (ActionContext actionContext) => - new BadRequestObjectResult( - new ApiError.AspBadRequest("Bad request", actionContext.ModelState).ToJson() - ); + options.InvalidModelStateResponseFactory = actionContext => new BadRequestObjectResult( + new ApiError.AspBadRequest("Bad request", actionContext.ModelState).ToJson() + ); }); builder diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs index 25d5327..225461c 100644 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs +++ b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs @@ -25,20 +25,20 @@ namespace Foxnouns.Backend.Services.Auth; public partial class FediverseAuthService { private string MastodonRedirectUri(string instance) => - $"{_config.BaseUrl}/auth/callback/mastodon/{instance}"; + $"{config.BaseUrl}/auth/callback/mastodon/{instance}"; private async Task CreateMastodonApplicationAsync( string instance, Snowflake? existingAppId = null ) { - HttpResponseMessage resp = await _client.PostAsJsonAsync( + HttpResponseMessage resp = await client.PostAsJsonAsync( $"https://{instance}/api/v1/apps", new CreateMastodonApplicationRequest( - $"pronouns.cc (+{_config.BaseUrl})", + $"pronouns.cc (+{config.BaseUrl})", MastodonRedirectUri(instance), "read read:accounts", - _config.BaseUrl + config.BaseUrl ) ); resp.EnsureSuccessStatusCode(); @@ -58,19 +58,19 @@ public partial class FediverseAuthService { app = new FediverseApplication { - Id = existingAppId ?? _snowflakeGenerator.GenerateSnowflake(), + Id = existingAppId ?? snowflakeGenerator.GenerateSnowflake(), ClientId = mastodonApp.ClientId, ClientSecret = mastodonApp.ClientSecret, Domain = instance, InstanceType = FediverseInstanceType.MastodonApi, }; - _db.Add(app); + db.Add(app); } else { app = - await _db.FediverseApplications.FindAsync(existingAppId) + await db.FediverseApplications.FindAsync(existingAppId) ?? throw new FoxnounsError($"Existing app with ID {existingAppId} was null"); app.ClientId = mastodonApp.ClientId; @@ -78,7 +78,7 @@ public partial class FediverseAuthService app.InstanceType = FediverseInstanceType.MastodonApi; } - await _db.SaveChangesAsync(); + await db.SaveChangesAsync(); return app; } @@ -90,9 +90,9 @@ public partial class FediverseAuthService ) { if (state != null) - await _keyCacheService.ValidateAuthStateAsync(state); + await keyCacheService.ValidateAuthStateAsync(state); - HttpResponseMessage tokenResp = await _client.PostAsync( + HttpResponseMessage tokenResp = await client.PostAsync( MastodonTokenUri(app.Domain), new FormUrlEncodedContent( new Dictionary @@ -123,7 +123,7 @@ public partial class FediverseAuthService var req = new HttpRequestMessage(HttpMethod.Get, MastodonCurrentUserUri(app.Domain)); req.Headers.Add("Authorization", $"Bearer {token}"); - HttpResponseMessage currentUserResp = await _client.SendAsync(req); + HttpResponseMessage currentUserResp = await client.SendAsync(req); currentUserResp.EnsureSuccessStatusCode(); FediverseUser? user = await currentUserResp.Content.ReadFromJsonAsync(); if (user == null) @@ -151,7 +151,7 @@ public partial class FediverseAuthService app = await CreateMastodonApplicationAsync(app.Domain, app.Id); } - state ??= HttpUtility.UrlEncode(await _keyCacheService.GenerateAuthStateAsync()); + state ??= HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync()); return $"https://{app.Domain}/oauth/authorize?response_type=code" + $"&client_id={app.ClientId}" diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Misskey.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Misskey.cs index 10a61e4..bf6c4b4 100644 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Misskey.cs +++ b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Misskey.cs @@ -34,11 +34,11 @@ public partial class FediverseAuthService Snowflake? existingAppId = null ) { - HttpResponseMessage resp = await _client.PostAsJsonAsync( + HttpResponseMessage resp = await client.PostAsJsonAsync( MisskeyAppUri(instance), new CreateMisskeyApplicationRequest( - $"pronouns.cc (+{_config.BaseUrl})", - $"pronouns.cc on {_config.BaseUrl}", + $"pronouns.cc (+{config.BaseUrl})", + $"pronouns.cc on {config.BaseUrl}", ["read:account"], MastodonRedirectUri(instance) ) @@ -60,19 +60,19 @@ public partial class FediverseAuthService { app = new FediverseApplication { - Id = existingAppId ?? _snowflakeGenerator.GenerateSnowflake(), + Id = existingAppId ?? snowflakeGenerator.GenerateSnowflake(), ClientId = misskeyApp.Id, ClientSecret = misskeyApp.Secret, Domain = instance, InstanceType = FediverseInstanceType.MisskeyApi, }; - _db.Add(app); + db.Add(app); } else { app = - await _db.FediverseApplications.FindAsync(existingAppId) + await db.FediverseApplications.FindAsync(existingAppId) ?? throw new FoxnounsError($"Existing app with ID {existingAppId} was null"); app.ClientId = misskeyApp.Id; @@ -80,7 +80,7 @@ public partial class FediverseAuthService app.InstanceType = FediverseInstanceType.MisskeyApi; } - await _db.SaveChangesAsync(); + await db.SaveChangesAsync(); return app; } @@ -96,7 +96,7 @@ public partial class FediverseAuthService private async Task GetMisskeyUserAsync(FediverseApplication app, string code) { - HttpResponseMessage resp = await _client.PostAsJsonAsync( + HttpResponseMessage resp = await client.PostAsJsonAsync( MisskeyTokenUri(app.Domain), new GetMisskeySessionUserKeyRequest(app.ClientSecret, code) ); @@ -130,7 +130,7 @@ public partial class FediverseAuthService app = await CreateMisskeyApplicationAsync(app.Domain, app.Id); } - HttpResponseMessage resp = await _client.PostAsJsonAsync( + HttpResponseMessage resp = await client.PostAsJsonAsync( MisskeyGenerateSessionUri(app.Domain), new CreateMisskeySessionUriRequest(app.ClientSecret) ); diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs index daa15ab..3ffc28d 100644 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs +++ b/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs @@ -19,33 +19,17 @@ using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; namespace Foxnouns.Backend.Services.Auth; -public partial class FediverseAuthService +public partial class FediverseAuthService( + ILogger logger, + Config config, + DatabaseContext db, + HttpClient client, + KeyCacheService keyCacheService, + ISnowflakeGenerator snowflakeGenerator +) { private const string NodeInfoRel = "http://nodeinfo.diaspora.software/ns/schema/2.0"; - - private readonly HttpClient _client; - private readonly ILogger _logger; - private readonly Config _config; - private readonly DatabaseContext _db; - private readonly KeyCacheService _keyCacheService; - private readonly ISnowflakeGenerator _snowflakeGenerator; - - public FediverseAuthService( - ILogger logger, - Config config, - DatabaseContext db, - HttpClient client, - KeyCacheService keyCacheService, - ISnowflakeGenerator snowflakeGenerator - ) - { - _logger = logger.ForContext(); - _config = config; - _db = db; - _keyCacheService = keyCacheService; - _snowflakeGenerator = snowflakeGenerator; - _client = client; - } + private readonly ILogger _logger = logger.ForContext(); public async Task GenerateAuthUrlAsync( string instance, @@ -66,7 +50,7 @@ public partial class FediverseAuthService public async Task GetApplicationAsync(string instance) { - FediverseApplication? app = await _db.FediverseApplications.FirstOrDefaultAsync(a => + FediverseApplication? app = await db.FediverseApplications.FirstOrDefaultAsync(a => a.Domain == instance ); if (app != null) @@ -88,7 +72,7 @@ public partial class FediverseAuthService { _logger.Debug("Requesting software name for fediverse instance {Instance}", instance); - HttpResponseMessage wellKnownResp = await _client.GetAsync( + HttpResponseMessage wellKnownResp = await client.GetAsync( new Uri($"https://{instance}/.well-known/nodeinfo") ); wellKnownResp.EnsureSuccessStatusCode(); @@ -103,7 +87,7 @@ public partial class FediverseAuthService ); } - HttpResponseMessage nodeInfoResp = await _client.GetAsync(nodeInfoUrl); + HttpResponseMessage nodeInfoResp = await client.GetAsync(nodeInfoUrl); nodeInfoResp.EnsureSuccessStatusCode(); PartialNodeInfo? nodeInfo = await nodeInfoResp.Content.ReadFromJsonAsync(); diff --git a/Foxnouns.Backend/Services/KeyCacheService.cs b/Foxnouns.Backend/Services/KeyCacheService.cs index 228a8fc..5c59bce 100644 --- a/Foxnouns.Backend/Services/KeyCacheService.cs +++ b/Foxnouns.Backend/Services/KeyCacheService.cs @@ -39,8 +39,6 @@ public class KeyCacheService(Config config) public async Task DeleteKeyAsync(string key) => await Multiplexer.GetDatabase().KeyDeleteAsync(key); - public Task DeleteExpiredKeysAsync(CancellationToken ct) => Task.CompletedTask; - public async Task SetKeyAsync(string key, T obj, Duration expiresAt) where T : class { diff --git a/Foxnouns.Backend/Services/PeriodicTasksService.cs b/Foxnouns.Backend/Services/PeriodicTasksService.cs index bf0f4af..e5efd28 100644 --- a/Foxnouns.Backend/Services/PeriodicTasksService.cs +++ b/Foxnouns.Backend/Services/PeriodicTasksService.cs @@ -33,11 +33,9 @@ public class PeriodicTasksService(ILogger logger, IServiceProvider services) : B // The type is literally written on the same line, we can just use `var` // ReSharper disable SuggestVarOrType_SimpleTypes - var keyCacheService = scope.ServiceProvider.GetRequiredService(); var dataCleanupService = scope.ServiceProvider.GetRequiredService(); // ReSharper restore SuggestVarOrType_SimpleTypes - await keyCacheService.DeleteExpiredKeysAsync(ct); await dataCleanupService.InvokeAsync(ct); } } diff --git a/Foxnouns.Backend/Services/ValidationService.Strings.cs b/Foxnouns.Backend/Services/ValidationService.Strings.cs index 8f43052..9244ed4 100644 --- a/Foxnouns.Backend/Services/ValidationService.Strings.cs +++ b/Foxnouns.Backend/Services/ValidationService.Strings.cs @@ -31,6 +31,7 @@ public partial class ValidationService "settings", "pronouns.cc", "pronounscc", + "null", ]; private static readonly string[] InvalidMemberNames = @@ -38,8 +39,10 @@ public partial class ValidationService // these break routing outright ".", "..", - // the user edit page lives at `/@{username}/edit`, so a member named "edit" would be inaccessible + // TODO: remove this? i'm not sure if /@[username]/edit will redirect to settings "edit", + // this breaks the frontend, somehow + "null", ]; public ValidationError? ValidateUsername(string username) From f00f5b400e78d9d458315a2bebefdbdc45c8203f Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 17 Mar 2025 22:46:44 +0100 Subject: [PATCH 3/3] feat(frontend): allow configuring assets url --- Foxnouns.Frontend/package.json | 1 + Foxnouns.Frontend/pnpm-lock.yaml | 3 +++ Foxnouns.Frontend/svelte.config.js | 12 ++++++++++++ 3 files changed, 16 insertions(+) diff --git a/Foxnouns.Frontend/package.json b/Foxnouns.Frontend/package.json index a76a918..013e44f 100644 --- a/Foxnouns.Frontend/package.json +++ b/Foxnouns.Frontend/package.json @@ -21,6 +21,7 @@ "@types/markdown-it": "^14.1.2", "@types/sanitize-html": "^2.13.0", "bootstrap": "^5.3.3", + "dotenv": "^16.4.7", "eslint": "^9.17.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.46.1", diff --git a/Foxnouns.Frontend/pnpm-lock.yaml b/Foxnouns.Frontend/pnpm-lock.yaml index 46b0010..68beeed 100644 --- a/Foxnouns.Frontend/pnpm-lock.yaml +++ b/Foxnouns.Frontend/pnpm-lock.yaml @@ -72,6 +72,9 @@ importers: bootstrap: specifier: ^5.3.3 version: 5.3.3(@popperjs/core@2.11.8) + dotenv: + specifier: ^16.4.7 + version: 16.4.7 eslint: specifier: ^9.17.0 version: 9.17.0 diff --git a/Foxnouns.Frontend/svelte.config.js b/Foxnouns.Frontend/svelte.config.js index fcd662a..f4ddf37 100644 --- a/Foxnouns.Frontend/svelte.config.js +++ b/Foxnouns.Frontend/svelte.config.js @@ -1,5 +1,14 @@ import adapter from "@sveltejs/adapter-node"; import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; +import * as path from "node:path"; + +import { config as dotenv } from "dotenv"; +dotenv({ + path: [path.resolve(process.cwd(), ".env"), path.resolve(process.cwd(), ".env.local")], +}); + +console.log(process.env.NODE_ENV); +const isProd = process.env.NODE_ENV === "production"; /** @type {import('@sveltejs/kit').Config} */ const config = { @@ -21,6 +30,9 @@ const config = { // we only disable it during development, during building NODE_ENV == production checkOrigin: process.env.NODE_ENV !== "development", }, + paths: { + assets: isProd ? process.env.PRIVATE_ASSETS_PREFIX || "" : "", + }, }, };