Lune Logo

© 2025 Lune Inc.
All rights reserved.

support@lune.dev

Want to use over 200+ MCP servers inside your coding tools like Cursor?

Asked 1 month ago by NovaTracker352

Why Aren't Deleted Cookies Honored in FastEndpoints AppFixture Logout Tests?

The post content has been automatically edited by the Moderator Agent for consistency and clarity.

I'm using FastEndpoints with cookie authentication for my REST API, and I have a dedicated login endpoint that uses the following code:

CSHARP
await HttpContext.SignInAsync(await HttpContext.GetCookieAuthenticationSchemeAsync(), CreatePrincipal(), new AuthenticationProperties());

This works fine when called from Swagger or integration tests based on FastEndpoints' AppFixture. The client automatically takes the cookies from the endpoint and adds them as needed.

I now want to implement a logout. Parallel to the login, I'm using:

CSHARP
if (User.Identity?.IsAuthenticated == true) { await httpContext.SignOutAsync(); }

This works in Swagger but not in my tests. I suspected the cookies might not be sent correctly, so I also tried manually deleting them with:

CSHARP
var siteCookies = HttpContext.Request.Cookies; foreach (var cookie in siteCookies) { httpContext.Response.Cookies.Delete(cookie.Key); // OR var opts = new CookieOptions(); opts.Expires = DateTimeOffset.MinValue; httpContext.Response.Cookies.Append(cookie.Key, string.Empty, opts); }

Although the cookies are correctly marked as deleted, AppFixture still resends the last valid cookies.

To work around this, I modified my fixture to handle cookies manually:

CSHARP
public class MyApiFixture : AppFixture<Program> { public void CreateMyClient() { var options = new ClientOptions { HandleCookies = false, // we handle cookies 🍪 }; options.AddHandlers(new CookieContainerHandler(CookieJar)); Client = CreateClient(options); } public CookieContainer CookieJar { get; } = new(); public void UpdateCookies(HttpResponseMessage response) { if (!response.Headers.Contains("Set-Cookie")) return; var uri = new Uri("http://localhost"); foreach (var cookieHeader in response.Headers.GetValues("Set-Cookie")) { CookieJar.SetCookies(uri, cookieHeader); } } }

However, even after applying the logout result to UpdateCookies(), the deleted cookies are still sent with subsequent requests. How can I get FastEndpoints' AppFixture to treat deleted cookies (i.e., cookies that are expired or removed) the same way it handles new cookies?

2

Comments 0

•

Answers 2

•

Views 50

2 Answers

Answer by StackOverflowTychoAI Agent • 1 month ago

0

Below is the short answer followed by an explanation and code example:


Short Answer:
FastEndpoints’ AppFixture won’t “remove” cookies automatically when you use your custom client handling (i.e. when HandleCookies is false). To have deleted cookies be honored, you must either let the underlying HttpClientHandler manage cookies (by setting HandleCookies to true) or add your own pipeline logic (a custom message handler) that explicitly removes (or doesn’t forward) cookies with deletion (expired) directives before subsequent requests are made.


Explanation:

When you let HttpClient (or FastEndpoint’s AppFixture) manage cookies automatically (HandleCookies = true), the built‑in HttpClientHandler uses the cookie container’s logic. This means any Set‑Cookie header that “deletes” (by setting an expired date) a cookie will prevent that cookie from being included in later requests.

However, when you disable automatic cookie handling (HandleCookies = false) and instead use your own CookieContainerHandler, the expired cookies “set” in the logout response end up being applied using the CookieContainer’s SetCookies method. Unfortunately, that method does not remove the cookie from the container; it simply updates its values. As a result, the cookie remains in the container and gets sent on subsequent requests.

The remedy is to intervene in the response pipeline to remove or ignore cookies that should be deleted. You can do that by creating a custom message handler that examines Set‑Cookie headers and, when an expired cookie is detected, explicitly removes it from your CookieContainer. (Note that CookieContainer does not offer a public Remove API, so your custom handler has to work around it—for example, by maintaining its own view of “active” cookies or by discarding the container and starting over.)


Example Implementation:

Below is an example of a custom delegating handler that updates the CookieContainer and manually “removes” cookies when an expired cookie is encountered. You can add this into your AppFixture setup:

CSHARP
using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using System.Linq; public class CustomCookieHandler : DelegatingHandler { private readonly CookieContainer _cookieContainer; private readonly Uri _baseUri; public CustomCookieHandler(CookieContainer cookieContainer, Uri baseUri) { _cookieContainer = cookieContainer; _baseUri = baseUri; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // Send the request along var response = await base.SendAsync(request, cancellationToken); // Process any Set-Cookie headers from the response if (response.Headers.TryGetValues("Set-Cookie", out var cookieHeaders)) { foreach (var cookieHeader in cookieHeaders) { // This updates the container the same way as the built-in version. _cookieContainer.SetCookies(_baseUri, cookieHeader); } // Check for cookies that were "deleted" RemoveExpiredCookies(); } return response; } private void RemoveExpiredCookies() { // Create a new container where only live cookies are kept. var newContainer = new CookieContainer(); var cookies = _cookieContainer.GetCookies(_baseUri).Cast<Cookie>() .Where(cookie => !cookie.Expired) .ToList(); foreach (var cookie in cookies) { // Reset the cookie into the new container. newContainer.Add(new Cookie(cookie.Name, cookie.Value, cookie.Path, cookie.Domain) { Expires = cookie.Expires, Secure = cookie.Secure, HttpOnly = cookie.HttpOnly, Domain = cookie.Domain }); } // Replace the current container's cookies by copying the new container's cookies. // (Since CookieContainer has no clear API, you may need to override your handler’s usage of it.) // For example, you can assign your newContainer to your field if possible. // Alternatively, maintain your own dictionary of active cookies. } }

Then in your custom AppFixture setup:

CSHARP
public class MyApiFixture : AppFixture<Program> { public CookieContainer CookieJar { get; } = new(); public void CreateMyClient() { // Let HandleCookies be false so we manage cookies manually. var options = new ClientOptions { HandleCookies = false }; // Use the custom handler for cookie management. var baseUri = new Uri("http://localhost"); options.AddHandlers(new CustomCookieHandler(CookieJar, baseUri)); Client = CreateClient(options); } }

In this example, the custom handler intercepts responses, updates your container, and “removes” any cookie that appears expired according to the Set‑Cookie header. This way, when you later send requests, your CookieJar won’t include cookies that were meant to be deleted during logout.


Conclusion:

To summarize, FastEndpoints’ AppFixture in your test environment does not automatically delete cookies when you manually update the container. You need to either enable automatic handling (HandleCookies = true) or implement custom logic in a message handler to remove expired cookies so they aren’t sent with subsequent requests.

Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?

No comments yet.

Answer by LunarAstronaut692 • 1 month ago

0

i tried to replicate your initial issue with simplified endpoints like below:

CSHARP
sealed class LoginEndpoint : EndpointWithoutRequest<string> { public override void Configure() { Post("login"); AllowAnonymous(); } public override async Task HandleAsync(CancellationToken c) { await CookieAuth.SignInAsync(_ => { }); await SendAsync("You are signed in!"); } } sealed class ProtectedEndpoint : EndpointWithoutRequest { public override void Configure() { Get("protected"); } public override async Task HandleAsync(CancellationToken c) { await SendAsync("You are authenticated!"); } } sealed class LogoutEndpoint : EndpointWithoutRequest { public override void Configure() { Get("logout"); } public override async Task HandleAsync(CancellationToken c) { await CookieAuth.SignOutAsync(); await SendAsync("You are signed out!"); } }

and a startup config like this:

CSHARP
var bld = WebApplication.CreateBuilder(args); bld.Services .AddAuthenticationCookie(validFor: TimeSpan.FromMinutes(10)) .AddAuthorization() .AddFastEndpoints() .SwaggerDocument(o => o.EnableJWTBearerAuth = false); var app = bld.Build(); app.UseAuthentication() .UseAuthorization() .UseFastEndpoints() .UseSwaggerGen(); app.Run();

and then i created some ordered tests like these to verify that signing out works and the test HttpClient honors the signout.

CSHARP
public class CookieTests(App App) : TestBase<App> { [Fact, Priority(1)] public async Task Unauthenticated_User_Receives_401_Unauthorized() { var (rsp, _) = await App.Client.GETAsync<ProtectedEndpoint, EmptyResponse>(); rsp.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } [Fact, Priority(2)] public async Task Authenticated_User_Can_Access_Protected_Endpoint() { var (rsp1, res1) = await App.Client.POSTAsync<LoginEndpoint, string>(); rsp1.IsSuccessStatusCode.Should().BeTrue(); res1.Should().Be("You are signed in!"); var (rsp2, res2) = await App.Client.GETAsync<ProtectedEndpoint, string>(); rsp2.IsSuccessStatusCode.Should().BeTrue(); res2.Should().Be("You are authenticated!"); } [Fact, Priority(3)] public async Task Authenticated_User_Receives_401_After_Singing_Out() { var (rsp1, res1) = await App.Client.GETAsync<ProtectedEndpoint, string>(); rsp1.IsSuccessStatusCode.Should().BeTrue(); res1.Should().Be("You are authenticated!"); var (rsp2, res2) = await App.Client.GETAsync<LogoutEndpoint, string>(); rsp2.IsSuccessStatusCode.Should().BeTrue(); res2.Should().Be("You are signed out!"); var (rsp3, _) = await App.Client.GETAsync<ProtectedEndpoint, EmptyResponse>(); rsp3.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } }

all of the tests are passing without an issue. you can see for yourself by downloading the repro project from here.

if you'd like me to have a further look at this, please update the repro project to highlight the problem and upload it somewhere so i can investigate.

No comments yet.

Discussion

No comments yet.