← All tips

Prevent Duplicate POST Submissions with Claude Code

🤖

Curated by Jepoy  ·  AI-Generated Content

This article was autonomously generated by an AI pipeline designed and built by Jepoy. The author created the system, prompts, and infrastructure that produces this content — not the article itself. Content is intended for educational purposes and may contain inaccuracies. Always verify technical details before applying in production.

Prevent Duplicate POST Submissions with Claude Code

It’s a common and frustrating pain point in web development: users accidentally double-clicking a submit button or transient network issues causing a POST request to be sent twice. Without careful handling, this can lead to duplicate data entries, inconsistent application state, and a poor user experience. For APIs performing critical operations, ensuring idempotency on POST requests isn’t just a good idea; it’s paramount. While you could painstakingly build this logic manually for each endpoint, leveraging AI like Claude Code can significantly accelerate the process by generating a reusable ASP.NET Core middleware.

We can use Claude Code to create an ASP.NET Core middleware that effectively tracks recent POST requests and prevents duplicates. The core challenge lies in uniquely identifying requests to detect duplicates. A robust approach typically involves a combination of the request path, HTTP method, and a unique identifier submitted by the client, often within the request body or headers. For this example, we’ll assume the client includes a clientRequestId in their POST payloads. Claude Code can generate the foundational structure for an IdempotentPostMiddleware class, designed to inspect this identifier and utilize a distributed cache to record and check against previously seen requests.

To guide Claude Code, consider a prompt like: “Generate an ASP.NET Core middleware in C# named IdempotentPostMiddleware that intercepts POST requests. It should accept an IOptions<IdempotencyOptions> for configuration and use an IDistributedCache to store unique identifiers. These identifiers should be derived from the request path and a clientRequestId extracted from the JSON request body. If a request with an identical identifier has been processed recently (within a configurable duration), the middleware should return a 409 Conflict status code. Otherwise, it should permit the request to proceed to the next middleware and store the request identifier in the cache.”

// Example middleware generated by Claude Code (simplified for brevity)
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;

public class IdempotentPostMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IDistributedCache _cache;
    private readonly IdempotencyOptions _options;

    public IdempotentPostMiddleware(RequestDelegate next, IDistributedCache cache, IOptions<IdempotencyOptions> options)
    {
        _next = next;
        _cache = cache;
        _options = options.Value;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Only intercept POST requests
        if (context.Request.Method == HttpMethods.Post)
        {
            // Reading the request body requires care as it can only be read once.
            // We'll read it, process it, and then reset the stream for subsequent handlers.
            string requestBody;
            using (var reader = new StreamReader(context.Request.Body, System.Text.Encoding.UTF8, leaveOpen: true))
            {
                requestBody = await reader.ReadToEndAsync();
            }
            
            // Reset the stream to allow subsequent middleware/handlers to read it.
            var memoryStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(requestBody));
            context.Request.Body = memoryStream;

            // Attempt to extract the client-provided unique request identifier
            string clientId = null;
            try
            {
                var jsonDocument = JsonDocument.Parse(requestBody);
                if (jsonDocument.RootElement.TryGetProperty("clientRequestId", out var requestIdElement) && requestIdElement.ValueKind == JsonValueKind.String)
                {
                    clientId = requestIdElement.GetString();
                }
            }
            catch (JsonException)
            {
                // Handle cases where the body is not valid JSON, or doesn't contain the expected field
                // For simplicity, we'll proceed without idempotency check if parsing fails.
            }

            if (!string.IsNullOrEmpty(clientId))
            {
                // Construct a cache key unique to the request path and client ID.
                string cacheKey = $"idempotency:{context.Request.Path}:{clientId}";

                // Check if this request identifier has already been processed recently.
                var existingEntry = await _cache.GetStringAsync(cacheKey);
                if (existingEntry != null)
                {
                    // If found, return a 409 Conflict to indicate it's a duplicate.
                    context.Response.StatusCode = StatusCodes.Status409Conflict;
                    await context.Response.WriteAsync("Request already processed.");
                    return; // Stop further processing for this duplicate request.
                }

                // If not a duplicate, cache the identifier with an expiration time.
                await _cache.SetStringAsync(cacheKey, "processed", new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_options.CacheDurationInMinutes)
                });
            }
        }

        // If not a POST request, or if it's a POST request without a detectable clientRequestId,
        // proceed to the next middleware in the pipeline.
        await _next(context);
    }
}

public class IdempotencyOptions
{
    public int CacheDurationInMinutes { get; set; } = 5; // Default to 5 minutes
}

A critical consideration, and a potential “gotcha” for production readiness, is the uniqueness and reliability of the clientRequestId. If clients fail to provide it, or if the generated IDs are not truly unique across all potential duplicate submissions, idempotency won’t be guaranteed. Furthermore, careful attention must be paid to network timing, the chosen distributed cache’s availability and performance, and implementing appropriate cache expiration strategies to prevent unbounded memory growth and to ensure legitimate subsequent requests are eventually allowed after a sufficient period. This approach works by establishing a shared state (via the distributed cache) that tracks processed requests based on a unique identifier. If the same identifier appears again within the cache’s expiry window, it signifies a duplicate submission.

To implement this, integrate the generated IdempotentPostMiddleware into your ASP.NET Core application’s middleware pipeline, typically in Startup.cs or Program.cs. You’ll also need to configure your chosen IDistributedCache implementation (e.g., Redis or SQL Server). Once set up, test by sending two identical POST requests with the same clientRequestId to a target endpoint. You should observe the first request succeed, and the second attempt trigger a 409 Conflict response, demonstrating the middleware’s effectiveness in preventing duplicate submissions. This pattern offers a robust and maintainable solution that goes beyond basic API documentation by addressing a common, complex development challenge.