Add QuotaExceededException for 403 responses with Retry-After header
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:
QuotaExceededExceptionextendsFatalProtocolExceptionwithRetryAfterproperty exposing parsed wait time -
Detection logic:
PackageUpdateResource.EnsureSuccessStatusCodenow checks for 403 + Retry-After header before callingresponse.EnsureSuccessStatusCode() -
Header parsing: Added
GetRetryAfterhelper 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 enabledirective toQuotaExceededExceptionwith proper nullability annotations in PublicAPI files using!for non-nullable reference types -
Serialization: Implemented
GetObjectDataoverride for proper serialization of theRetryAfterproperty -
Localization: Error messages are now defined in
Strings.resx(QuotaExceeded_RetryAfterSecondsandQuotaExceeded_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!) andGetObjectDatamethod 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.Protocolpackage it throws just aHttpRequestExceptionthat just contains Status code. To get time I have to get last Forbidden message from customNuGet.Common.ILoggerand 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 build again but set the environment variable UpdateXlfOnBuild=true which will update the .xlf files and you can include them in the next commit
@copilot build again but set the environment variable
UpdateXlfOnBuild=truewhich will update the.xlffiles 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:
- Configure Actions setup steps to set up my environment, which run before the firewall is enabled
- Add the appropriate URLs or hosts to the custom allowlist in this repository's Copilot coding agent settings (admins only)
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.