Skip to content

Send a User-Agent from every DNS provider HTTP client - #1232

Merged
shibayan merged 1 commit into
masterfrom
fix/dns-provider-user-agent
Jul 30, 2026
Merged

Send a User-Agent from every DNS provider HTTP client#1232
shibayan merged 1 commit into
masterfrom
fix/dns-provider-user-agent

Conversation

@shibayan

Copy link
Copy Markdown
Member

Summary

  • DNS providers that build an HttpClient directly sent no User-Agent, which the IONOS API edge rejects with a 503 before authentication. Add a shared factory so every provider identifies itself as Acmebot/{version}.

Related Issue

What Changed

  • Added DnsProviderHttpClient, a shared factory that sets the base address, the application/json Accept header, and a User-Agent of Acmebot/{Constants.ApplicationVersion} — the same form AcmeClientFactory already sends to ACME endpoints.
  • Switched the eleven providers that constructed HttpClient inline over to the factory: Cloudflare, Custom DNS, DNS Made Easy, Gandi LiveDNS, GoDaddy, IONOS, OVH, PowerDNS, Regfish, TransIP, UnitedDomains. Each dropped its own BaseAddress + Accept setup; auth headers are unchanged.
  • Akamai Edge DNS receives its client from EdgeGridSigner.CreateHttpClient(), so the header is applied to that instance with DnsProviderHttpClient.AddUserAgent(). The library sets no User-Agent of its own, so nothing is appended to an existing value.
  • The TransIP token-acquisition client inside TransIpSignHandler is covered as well.
  • UnitedDomainsProvider previously sent the bare string acmebot with no version; it now uses the shared versioned value.
  • Removed the using System.Net.Http.Headers; directive that became unused in seven files.
  • Added DnsProviderHttpClientTests covering the base address, the Accept header, and the User-Agent on 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 errors
  • dotnet format --verify-no-changes --verbosity detailed --no-restore ./Acmebot.slnx
  • dotnet test — 126 passed, 0 failed
  • az bicep build -f ./deploy/azuredeploy.bicep — not run; no Bicep or deployment files changed
  • Documentation updated if needed — no user-facing configuration change, so no docs update

Notes

Reproduced and confirmed against the live IONOS endpoint with a deliberately invalid key, matching the report in #1230:

no UA         : ServiceUnavailable   (503, rejected at the edge)
Acmebot/5.1.2 : Unauthorized         (401, reached authentication)

Two adjacent observations left out of scope:

  • WebhookInvoker uses IHttpClientFactory and still sends no User-Agent. It is not a DNS provider, but the same class of rejection could affect notification endpoints.
  • [Bug]: IONOS DNS provider fails. Missing User-Agent gets 503 from the API #1230 also notes that the failure is invisible because the zone query swallows the exception. DnsZoneQueryService does log it via LogDnsZoneListingFailed, so it is not entirely silent, but it still returns an empty list to the caller. Worth deciding on separately.

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings July 30, 2026 11:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 DnsProviderHttpClient factory to apply consistent BaseAddress, Accept: application/json, and User-Agent.
  • Updated DNS providers that previously created HttpClient inline to use the shared factory (or to have the User-Agent applied to externally-created clients).
  • Added unit tests validating the shared factory and the AddUserAgent helper.

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.

@shibayan shibayan self-assigned this Jul 30, 2026
@shibayan shibayan added the bug Something isn't working label Jul 30, 2026
@shibayan
shibayan merged commit e6b1aa5 into master Jul 30, 2026
11 checks passed
@shibayan
shibayan deleted the fix/dns-provider-user-agent branch July 30, 2026 11:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: IONOS DNS provider fails. Missing User-Agent gets 503 from the API

2 participants