Send a User-Agent from every DNS provider HTTP client - #1232
Merged
Conversation
Of the eleven providers that build an HttpClient directly, only UnitedDomains
sent a User-Agent, and it sent the bare string "acmebot" with no version. The
IONOS API edge rejects requests without a User-Agent with a 503 before
authentication is attempted, so zone listing always came back empty and DNS-01
could never complete.
Add a shared DnsProviderHttpClient factory that sets the base address, the JSON
Accept header, and a User-Agent of Acmebot/{version} matching the one
AcmeClientFactory already sends to ACME endpoints. Centralizing the defaults
keeps the header from being forgotten by the next provider.
Akamai Edge DNS gets its client from EdgeGridSigner, which sets no User-Agent
of its own, so the header is applied to that instance afterwards. The TransIP
token client inside TransIpSignHandler is covered too. Azure DNS, Azure Private
DNS, Route 53 and Google Cloud DNS are unaffected because their SDKs send their
own User-Agent.
Verified against the live IONOS endpoint with a bogus key: no User-Agent
returns 503, Acmebot/5.1.2 reaches authentication and returns 401.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This pull request standardizes DNS-provider HTTP client construction so every provider sends a User-Agent header (Acmebot/{version}), addressing APIs (notably IONOS) that reject requests without a User-Agent before authentication.
Changes:
- Added a shared
DnsProviderHttpClientfactory to apply consistentBaseAddress,Accept: application/json, andUser-Agent. - Updated DNS providers that previously created
HttpClientinline to use the shared factory (or to have theUser-Agentapplied to externally-created clients). - Added unit tests validating the shared factory and the
AddUserAgenthelper.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Acmebot.App.Tests/DnsProviderHttpClientTests.cs | Adds coverage for base address, JSON Accept header, and User-Agent across factory/helper paths. |
| src/Acmebot.App/Providers/DnsProviderHttpClient.cs | Introduces centralized HttpClient creation + User-Agent application for DNS providers. |
| src/Acmebot.App/Providers/AkamaiEdgeDnsProvider.cs | Applies the shared User-Agent to an SDK-created client. |
| src/Acmebot.App/Providers/CloudflareProvider.cs | Switches to the shared DNS-provider HttpClient factory. |
| src/Acmebot.App/Providers/CustomDnsProvider.cs | Switches to the shared DNS-provider HttpClient factory. |
| src/Acmebot.App/Providers/DnsMadeEasyProvider.cs | Switches to the shared DNS-provider HttpClient factory. |
| src/Acmebot.App/Providers/GandiLiveDnsProvider.cs | Switches to the shared DNS-provider HttpClient factory. |
| src/Acmebot.App/Providers/GoDaddyProvider.cs | Switches to the shared DNS-provider HttpClient factory. |
| src/Acmebot.App/Providers/IonosDnsProvider.cs | Switches to the shared DNS-provider HttpClient factory to ensure User-Agent is sent. |
| src/Acmebot.App/Providers/OvhProvider.cs | Switches to the shared DNS-provider HttpClient factory. |
| src/Acmebot.App/Providers/PowerDnsProvider.cs | Switches to the shared DNS-provider HttpClient factory. |
| src/Acmebot.App/Providers/RegfishProvider.cs | Switches to the shared DNS-provider HttpClient factory. |
| src/Acmebot.App/Providers/TransIpProvider.cs | Switches both main and token-acquisition clients to the shared factory to ensure User-Agent is sent. |
| src/Acmebot.App/Providers/UnitedDomainsProvider.cs | Switches to the shared DNS-provider HttpClient factory (replacing the previous unversioned UA). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
HttpClientdirectly sent noUser-Agent, which the IONOS API edge rejects with a503before authentication. Add a shared factory so every provider identifies itself asAcmebot/{version}.Related Issue
What Changed
DnsProviderHttpClient, a shared factory that sets the base address, theapplication/jsonAccept header, and aUser-AgentofAcmebot/{Constants.ApplicationVersion}— the same formAcmeClientFactoryalready sends to ACME endpoints.HttpClientinline over to the factory: Cloudflare, Custom DNS, DNS Made Easy, Gandi LiveDNS, GoDaddy, IONOS, OVH, PowerDNS, Regfish, TransIP, UnitedDomains. Each dropped its ownBaseAddress+Acceptsetup; auth headers are unchanged.EdgeGridSigner.CreateHttpClient(), so the header is applied to that instance withDnsProviderHttpClient.AddUserAgent(). The library sets noUser-Agentof its own, so nothing is appended to an existing value.TransIpSignHandleris covered as well.UnitedDomainsProviderpreviously sent the bare stringacmebotwith no version; it now uses the shared versioned value.using System.Net.Http.Headers;directive that became unused in seven files.DnsProviderHttpClientTestscovering the base address, the Accept header, and theUser-Agenton all three creation paths.Azure DNS, Azure Private DNS, Route 53 and Google Cloud DNS are untouched — their SDKs send their own
User-Agent.Validation
dotnet build -c Release ./Acmebot.slnx— 0 warnings, 0 errorsdotnet format --verify-no-changes --verbosity detailed --no-restore ./Acmebot.slnxdotnet test— 126 passed, 0 failedaz bicep build -f ./deploy/azuredeploy.bicep— not run; no Bicep or deployment files changedNotes
Reproduced and confirmed against the live IONOS endpoint with a deliberately invalid key, matching the report in #1230:
Two adjacent observations left out of scope:
WebhookInvokerusesIHttpClientFactoryand still sends noUser-Agent. It is not a DNS provider, but the same class of rejection could affect notification endpoints.DnsZoneQueryServicedoes log it viaLogDnsZoneListingFailed, so it is not entirely silent, but it still returns an empty list to the caller. Worth deciding on separately.🤖 Generated with Claude Code