NuGet.Client icon indicating copy to clipboard operation
NuGet.Client copied to clipboard

Add QuotaExceededException for 403 responses with Retry-After header

Open Copilot opened this issue 3 months ago • 2 comments

Bug

Fixes: https://github.com/NuGet/Home/issues/14635

Description

When pushing packages to nuget.org, quota limit violations return HTTP 403 with a Retry-After header indicating when to retry. The existing code throws generic HttpRequestException, forcing callers to parse logs for timing information.

Changes:

  • New exception type: QuotaExceededException extends FatalProtocolException with RetryAfter property exposing parsed wait time
  • Detection logic: PackageUpdateResource.EnsureSuccessStatusCode now checks for 403 + Retry-After header before calling response.EnsureSuccessStatusCode()
  • Header parsing: Added GetRetryAfter helper supporting both delta (seconds) and absolute date formats
  • Tests: Three scenarios covering delta format, date format, and 403 without Retry-After (ensures backward compatibility)
  • Nullability: Added #nullable enable directive to QuotaExceededException with proper nullability annotations in PublicAPI files using ! for non-nullable reference types
  • Serialization: Implemented GetObjectData override for proper serialization of the RetryAfter property
  • Localization: Error messages are now defined in Strings.resx (QuotaExceeded_RetryAfterSeconds and QuotaExceeded_RetryAfterHeader) to support localization
  • XLF files: Updated all 13 language xlf files (cs, de, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant) with new string resources marked as state="new" for translation
  • PublicAPI: Updated PublicAPI.Unshipped.txt files for all target frameworks (net8.0, net472, netstandard2.0) with correct nullability annotations (string!, System.Exception!, System.Runtime.Serialization.SerializationInfo!) and GetObjectData method declaration

Usage example:

try 
{
    await resource.Push(packagePaths, ...);
}
catch (QuotaExceededException ex)
{
    // ex.RetryAfter contains TimeSpan to wait
    Console.WriteLine($"Quota exceeded. Retry in {ex.RetryAfter?.TotalSeconds} seconds");
    await Task.Delay(ex.RetryAfter ?? TimeSpan.FromMinutes(1));
}

PR Checklist

  • [x] Meaningful title, helpful description and a linked NuGet/Home issue
  • [x] Added tests
  • [ ] Link to an issue or pull request to update docs if this PR changes settings, environment variables, new feature, etc.
Original prompt

This section details on the original issue you should resolve

<issue_title>Add a custom exception when quota reached</issue_title> <issue_description>### NuGet Product(s) Involved

NuGet SDK

The Elevator Pitch

Hello! When quota reached your sevice produces a 403 response with body text that contains time to try again. When pushing packages using NuGet.Protocol package it throws just a HttpRequestException that just contains Status code. To get time I have to get last Forbidden message from custom NuGet.Common.ILogger and parse it.

Please add custom Exception when quota exceeded with time from response body.

Additional Context and Details

My employer want to push release packages to nuget.org but there are about 1000 packages. I want to do it in one pipeline call with waiting for time after quota exceeded.</issue_description>

<agent_instructions>Implement a new exception when this happens, do not parse the body of the message and instead look at the response code and headers. Add a unit test</agent_instructions>

Comments on the Issue (you are @copilot in this section)

  • Fixes NuGet/Home#14635

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot avatar Nov 12 '25 17:11 Copilot

@copilot build again but set the environment variable UpdateXlfOnBuild=true which will update the .xlf files and you can include them in the next commit

jeffkl avatar Nov 12 '25 19:11 jeffkl

@copilot build again but set the environment variable UpdateXlfOnBuild=true which will update the .xlf files and you can include them in the next commit

Updated all 13 xlf files in 28010df. Since the build environment has network issues accessing Azure DevOps feeds, I manually added the new string entries to all xlf files with state="new" for the two resources (QuotaExceeded_RetryAfterHeader and QuotaExceeded_RetryAfterSeconds).

[!WARNING]

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • 0t3vsblobprodcus362.vsblob.vsassets.io
    • Triggering command: dotnet build src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj -c Release (dns block)
  • 11vvsblobprodcus336.vsblob.vsassets.io
    • Triggering command: dotnet build src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj -c Release (dns block)
  • 1k9vsblobprodcus379.vsblob.vsassets.io
    • Triggering command: dotnet build src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj -c Release (dns block)
  • 4myvsblobprodcus32.vsblob.vsassets.io
    • Triggering command: dotnet build src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj -c Release (dns block)
  • 6yfvsblobprodwus2121.vsblob.vsassets.io
    • Triggering command: dotnet build src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj -c Release (dns block)
  • frdvsblobprodcus327.vsblob.vsassets.io
    • Triggering command: dotnet build src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj -c Release (dns block)
  • gervsblobprodcus329.vsblob.vsassets.io
    • Triggering command: dotnet build src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj -c Release (dns block)
  • h6tvsblobprodcus346.vsblob.vsassets.io
    • Triggering command: dotnet build src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj -c Release (dns block)
  • ibzvsblobprodcus369.vsblob.vsassets.io
    • Triggering command: dotnet build src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj -c Release (dns block)
  • jd4vsblobprodcus366.vsblob.vsassets.io
    • Triggering command: dotnet build src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj -c Release (dns block)
  • jrqvsblobprodcus343.vsblob.vsassets.io
    • Triggering command: dotnet build src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj -c Release (dns block)
  • kmuvsblobprodcus389.vsblob.vsassets.io
    • Triggering command: dotnet build src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj -c Release (dns block)
  • l49vsblobprodcus358.vsblob.vsassets.io
    • Triggering command: dotnet build src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj -c Release (dns block)
  • m8dvsblobprodcus37.vsblob.vsassets.io
    • Triggering command: dotnet build src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj -c Release (dns block)
  • n3kvsblobprodcus335.vsblob.vsassets.io
    • Triggering command: dotnet build src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj -c Release (dns block)
  • p2ovsblobprodcus312.vsblob.vsassets.io
    • Triggering command: dotnet build src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj -c Release (dns block)
  • tphvsblobprodcus375.vsblob.vsassets.io
    • Triggering command: dotnet build src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj -c Release (dns block)
  • u3hvsblobprodcus371.vsblob.vsassets.io
    • Triggering command: dotnet build src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj -c Release (dns block)
  • v53vsblobprodcus320.vsblob.vsassets.io
    • Triggering command: dotnet build src/NuGet.Core/NuGet.Protocol/NuGet.Protocol.csproj -c Release (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot avatar Nov 12 '25 19:11 Copilot

This PR has been automatically marked as stale because it has no activity for 7 days. It will be closed if no further activity occurs within another 7 days of this comment. If it is closed, you may reopen it anytime when you're ready again, as long as you don't delete the branch.