Skip to content

Commit 0b8d299

Browse files
authored
feat(transport): send X-SDK-VERSION header (dotnet-sdk/{version}) (#139)
Send the NArk SDK's own version as X-SDK-VERSION: dotnet-sdk/{version} on every gRPC and REST request, alongside the existing X-Build-Version. Value derived from Nerdbank.GitVersioning with +commit metadata stripped.
1 parent 29d92af commit 0b8d299

3 files changed

Lines changed: 78 additions & 3 deletions

File tree

NArk.Core/Transport/ArkdVersion.cs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,64 @@ namespace NArk.Transport;
1010
public static class ArkdVersion
1111
{
1212
public const string TargetBuild = "0.9.9";
13+
14+
/// <summary>
15+
/// This SDK's own package version (computed by Nerdbank.GitVersioning from git history),
16+
/// sent as the <c>X-SDK-VERSION</c> header on every outgoing request. The SemVer build-metadata
17+
/// suffix (the <c>+commit</c> part of the assembly informational version) is stripped, leaving
18+
/// the clean version, e.g. <c>1.0.327-beta</c>. Unlike <see cref="TargetBuild"/> (the Arkade
19+
/// server build this SDK targets), this is the version of the NArk SDK itself.
20+
/// </summary>
21+
public static readonly string SdkVersion = StripBuildMetadata(ThisAssembly.AssemblyInformationalVersion);
22+
23+
/// <summary>
24+
/// Product token sent as the <c>X-SDK-VERSION</c> header, identifying this SDK and its version
25+
/// in <c>name/version</c> form, e.g. <c>dotnet-sdk/1.0.327-beta</c>. The name lets arkd
26+
/// distinguish the .NET SDK from other SDKs (e.g. the TypeScript SDK) on the same wire.
27+
/// </summary>
28+
public static readonly string SdkVersionHeaderValue = $"{SdkName}/{SdkVersion}";
29+
30+
internal const string SdkName = "dotnet-sdk";
1331
internal const string HeaderName = "X-Build-Version";
32+
internal const string SdkVersionHeaderName = "X-SDK-VERSION";
1433
internal const string DigestHeaderName = "X-Digest";
1534

1635
/// <summary>
17-
/// Adds the <c>X-Build-Version</c> default header to <paramref name="http"/>.
36+
/// Adds the <c>X-Build-Version</c> and <c>X-SDK-VERSION</c> default headers to <paramref name="http"/>.
1837
/// </summary>
1938
public static HttpClient InjectHeader(this HttpClient http)
2039
{
2140
if (!http.DefaultRequestHeaders.Contains(HeaderName))
2241
{
2342
http.DefaultRequestHeaders.TryAddWithoutValidation(HeaderName, TargetBuild);
2443
}
44+
if (!http.DefaultRequestHeaders.Contains(SdkVersionHeaderName))
45+
{
46+
http.DefaultRequestHeaders.TryAddWithoutValidation(SdkVersionHeaderName, SdkVersionHeaderValue);
47+
}
2548
return http;
2649
}
2750

2851
/// <summary>
29-
/// Appends the <c>X-Build-Version</c> entry to <paramref name="metadata"/>.
52+
/// Appends the <c>X-Build-Version</c> and <c>X-SDK-VERSION</c> entries to <paramref name="metadata"/>.
3053
/// </summary>
3154
internal static Metadata InjectHeader(this Metadata metadata)
3255
{
3356
metadata.Add(HeaderName, TargetBuild);
57+
metadata.Add(SdkVersionHeaderName, SdkVersionHeaderValue);
3458
return metadata;
3559
}
3660

61+
/// <summary>
62+
/// Strips the SemVer build-metadata suffix (everything from the first <c>+</c>) from an
63+
/// assembly informational version, e.g. <c>1.0.327-beta+d238a7c85b</c> → <c>1.0.327-beta</c>.
64+
/// </summary>
65+
private static string StripBuildMetadata(string informationalVersion)
66+
{
67+
var plus = informationalVersion.IndexOf('+');
68+
return plus < 0 ? informationalVersion : informationalVersion[..plus];
69+
}
70+
3771
/// <summary>
3872
/// Throws <see cref="IncompatibleSdkVersionException"/> when <paramref name="errorDetail"/> contains
3973
/// <c>BUILD_VERSION_TOO_OLD</c>. The exception propagates to the caller; the SDK does not catch it.

NArk.Tests/BuildVersionHeaderTests.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,46 @@ public void InjectHeader_HttpClient_AddsXBuildVersionHeader()
2424
Contains.Item(ArkdVersion.TargetBuild));
2525
}
2626

27+
[Test]
28+
public void SdkVersion_IsCleanSemVerWithoutBuildMetadata()
29+
{
30+
// nbgv computes this from git; the value changes per commit, so assert shape, not a literal.
31+
Assert.That(ArkdVersion.SdkVersion, Is.Not.Empty);
32+
Assert.That(ArkdVersion.SdkVersion, Does.Not.Contain("+"),
33+
"SdkVersion must be the clean version without the +commit build-metadata suffix.");
34+
}
35+
36+
[Test]
37+
public void SdkVersionHeaderValue_IsDotnetSdkProductToken()
38+
{
39+
// Product-token form "name/version", e.g. dotnet-sdk/1.0.327-beta.
40+
Assert.That(ArkdVersion.SdkVersionHeaderValue, Is.EqualTo($"dotnet-sdk/{ArkdVersion.SdkVersion}"));
41+
Assert.That(ArkdVersion.SdkVersionHeaderValue, Does.StartWith("dotnet-sdk/"));
42+
}
43+
44+
[Test]
45+
public void InjectHeader_HttpClient_AddsXSdkVersionHeader()
46+
{
47+
var http = new HttpClient();
48+
49+
http.InjectHeader();
50+
51+
Assert.That(
52+
http.DefaultRequestHeaders.GetValues("X-SDK-VERSION"),
53+
Contains.Item(ArkdVersion.SdkVersionHeaderValue));
54+
}
55+
56+
[Test]
57+
public void InjectHeader_HttpClient_XSdkVersionIsIdempotent()
58+
{
59+
var http = new HttpClient();
60+
61+
http.InjectHeader();
62+
http.InjectHeader();
63+
64+
Assert.That(http.DefaultRequestHeaders.GetValues("X-SDK-VERSION").ToList(), Has.Count.EqualTo(1));
65+
}
66+
2767
[Test]
2868
public void InjectHeader_HttpClient_IsIdempotent()
2969
{

docs/articles/signer-rotation.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,12 @@ Reconciliation calls `ISingleKeyDefaultEnsurer.EnsureDefaultAsync` to upsert the
4343

4444
## Version and Digest Headers
4545

46-
Every outgoing gRPC and REST request carries two headers:
46+
Every outgoing gRPC and REST request carries these headers:
4747

4848
| Header | Value | Purpose |
4949
|--------|-------|---------|
5050
| `X-Build-Version` | `ArkdVersion.TargetBuild` (e.g. `0.9.9`) | Lets arkd reject SDKs that are too old (`BUILD_VERSION_TOO_OLD`) |
51+
| `X-SDK-VERSION` | `ArkdVersion.SdkVersionHeaderValue` (e.g. `dotnet-sdk/1.0.327-beta`) | Identifies the SDK and its own package version (from Nerdbank.GitVersioning) in `name/version` form, so arkd can distinguish the .NET SDK from other SDKs |
5152
| `X-Digest` | Current server-info digest | Lets arkd detect stale cached configuration (`DIGEST_MISMATCH`) |
5253

5354
The headers are injected by `BuildVersionInterceptor` (gRPC) and `BuildVersionHandler` (REST). Both throw typed exceptions on rejection:

0 commit comments

Comments
 (0)