From 99de8a9da324eca706657aaee5bd3b3e4ddc6340 Mon Sep 17 00:00:00 2001 From: Arun Gopinathan Date: Wed, 26 Aug 2026 21:12:35 -0500 Subject: [PATCH] Fix TypeScript create precondition headers Use the standard If-None-Match header for tunnel and port creation, preserve caller-owned request options, and cover generated-ID conflict retries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c4ee497-a9b3-481b-860b-541eef9e9fba --- .../management/tunnelManagementHttpClient.ts | 19 ++++-- ts/test/tunnels-test/tunnelManagementTests.ts | 63 ++++++++++++++++++- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/ts/src/management/tunnelManagementHttpClient.ts b/ts/src/management/tunnelManagementHttpClient.ts index 892ca83a..fe70c891 100644 --- a/ts/src/management/tunnelManagementHttpClient.ts +++ b/ts/src/management/tunnelManagementHttpClient.ts @@ -349,8 +349,13 @@ export class TunnelManagementHttpClient implements TunnelManagementClient { } } - options.additionalHeaders = options.additionalHeaders || {}; - options.additionalHeaders['If-Not-Match'] = "*"; + options = { + ...options, + additionalHeaders: { + ...options.additionalHeaders, + 'If-None-Match': '*', + }, + }; if (idGenerated) { tunnel.tunnelId = IdGeneration.generateTunnelId(); @@ -578,9 +583,13 @@ export class TunnelManagementHttpClient implements TunnelManagementClient { this.raiseReportProgress(TunnelProgress.StartingCreateTunnelPort); tunnelPort = this.convertTunnelPortForRequest(tunnel, tunnelPort); const path = `${portsApiSubPath}/${tunnelPort.portNumber}`; - options = options || {}; - options.additionalHeaders = options.additionalHeaders || {}; - options.additionalHeaders['If-Not-Match'] = "*"; + options = { + ...options, + additionalHeaders: { + ...options?.additionalHeaders, + 'If-None-Match': '*', + }, + }; const result = (await this.sendTunnelRequest( 'PUT', tunnel, diff --git a/ts/test/tunnels-test/tunnelManagementTests.ts b/ts/test/tunnels-test/tunnelManagementTests.ts index 8cc4e23d..3fb5d2d8 100644 --- a/ts/test/tunnels-test/tunnelManagementTests.ts +++ b/ts/test/tunnels-test/tunnelManagementTests.ts @@ -6,7 +6,7 @@ import axios, { Axios, AxiosHeaders, AxiosError, AxiosPromise, AxiosRequestConfi import * as https from 'https'; import { suite, test, slow, timeout } from '@testdeck/mocha'; import { ManagementApiVersions, TunnelManagementHttpClient } from '@microsoft/dev-tunnels-management'; -import { Tunnel, TunnelProgress, TunnelReportProgressEventArgs, ClusterRecommendationResponse, ClusterAvailability } from '@microsoft/dev-tunnels-contracts'; +import { Tunnel, TunnelPort, TunnelProgress, TunnelReportProgressEventArgs, ClusterRecommendationResponse, ClusterAvailability } from '@microsoft/dev-tunnels-contracts'; import { CancellationToken, CancellationTokenSource } from 'vscode-jsonrpc'; @suite @@ -282,6 +282,7 @@ export class TunnelManagementTests { callCount++; const sentTunnel = config.data as Tunnel; + assert.strictEqual(config.headers?.['If-None-Match'], '*'); if (callCount === 1) { firstTunnelId = sentTunnel?.tunnelId; throw conflictError; @@ -315,6 +316,66 @@ export class TunnelManagementTests { } } + @test + public async createTunnelSendsIfNoneMatchHeader() { + const requestTunnel = { + tunnelId: 'tunnelid', + clusterId: 'clusterId', + }; + this.nextResponse = { + tunnelId: 'tunnelid', + clusterId: 'clusterId', + }; + const options = { + additionalHeaders: { + 'X-Test': 'value', + }, + }; + + await this.managementClient.createTunnel(requestTunnel, options); + + assert(this.lastRequest); + const headers = this.lastRequest!.config.headers as { [name: string]: string }; + assert.strictEqual(headers['If-None-Match'], '*'); + assert.strictEqual(headers['If-Not-Match'], undefined); + assert.strictEqual(headers['X-Test'], 'value'); + assert.strictEqual( + Object.prototype.hasOwnProperty.call(options.additionalHeaders, 'If-None-Match'), + false, + ); + } + + @test + public async createTunnelPortSendsIfNoneMatchHeader() { + const requestTunnel = { + tunnelId: 'tunnelid', + clusterId: 'clusterId', + }; + const requestPort = { + portNumber: 9900, + }; + this.nextResponse = { + portNumber: 9900, + }; + const options = { + additionalHeaders: { + 'X-Test': 'value', + }, + }; + + await this.managementClient.createTunnelPort(requestTunnel, requestPort, options); + + assert(this.lastRequest); + const headers = this.lastRequest!.config.headers as { [name: string]: string }; + assert.strictEqual(headers['If-None-Match'], '*'); + assert.strictEqual(headers['If-Not-Match'], undefined); + assert.strictEqual(headers['X-Test'], 'value'); + assert.strictEqual( + Object.prototype.hasOwnProperty.call(options.additionalHeaders, 'If-None-Match'), + false, + ); + } + @test public async getClusterRecommendationsReturnsResponse() { const response = {