diff --git a/packages/http-client/__tests__/basics.test.ts b/packages/http-client/__tests__/basics.test.ts index 254a8ec507..2bb57ed3bf 100644 --- a/packages/http-client/__tests__/basics.test.ts +++ b/packages/http-client/__tests__/basics.test.ts @@ -21,7 +21,10 @@ describe('basics', () => { _http = new httpm.HttpClient('http-client-tests') }) - afterEach(() => {}) + afterEach(() => { + // Clean up environment variable to prevent test pollution + delete process.env['ACTIONS_ORCHESTRATION_ID'] + }) it('constructs', () => { const http: httpm.HttpClient = new httpm.HttpClient('thttp-client-tests') @@ -60,7 +63,7 @@ describe('basics', () => { const body: string = await res.readBody() const obj = JSON.parse(body) expect(obj.url).toBe('https://postman-echo.com/get') - expect(obj.headers['user-agent']).toBeFalsy() + expect(obj.headers['user-agent']).toBe('actions/http-client') }) /* TODO write a mock rather then relying on a third party @@ -374,4 +377,66 @@ describe('basics', () => { httpm.MediaTypes.ApplicationJson ) }) + + it('appends orchestration ID to user-agent when ACTIONS_ORCHESTRATION_ID is set', async () => { + const orchId = 'test-orch-id-12345' + process.env['ACTIONS_ORCHESTRATION_ID'] = orchId + + const http: httpm.HttpClient = new httpm.HttpClient('http-client-tests') + const res: httpm.HttpClientResponse = await http.get( + 'https://postman-echo.com/get' + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + expect(obj.headers['user-agent']).toBe( + `http-client-tests actions_orchestration_id/${orchId}` + ) + }) + + it('sanitizes invalid characters in orchestration ID', async () => { + const orchId = 'test (with) special/chars' + process.env['ACTIONS_ORCHESTRATION_ID'] = orchId + + const http: httpm.HttpClient = new httpm.HttpClient('http-client-tests') + const res: httpm.HttpClientResponse = await http.get( + 'https://postman-echo.com/get' + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + // Spaces, parentheses, and slashes should be replaced with underscores + expect(obj.headers['user-agent']).toBe( + 'http-client-tests actions_orchestration_id/test__with__special_chars' + ) + }) + + it('does not modify user-agent when ACTIONS_ORCHESTRATION_ID is not set', async () => { + delete process.env['ACTIONS_ORCHESTRATION_ID'] + + const http: httpm.HttpClient = new httpm.HttpClient('http-client-tests') + const res: httpm.HttpClientResponse = await http.get( + 'https://postman-echo.com/get' + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + expect(obj.headers['user-agent']).toBe('http-client-tests') + }) + + it('uses default user-agent with orchestration ID when no custom user-agent provided', async () => { + const orchId = 'test-default-id-12345' + process.env['ACTIONS_ORCHESTRATION_ID'] = orchId + + const http: httpm.HttpClient = new httpm.HttpClient() + const res: httpm.HttpClientResponse = await http.get( + 'https://postman-echo.com/get' + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + expect(obj.headers['user-agent']).toBe( + `actions/http-client actions_orchestration_id/${orchId}` + ) + }) }) diff --git a/packages/http-client/src/index.ts b/packages/http-client/src/index.ts index 5691e89137..2aee17dc3e 100644 --- a/packages/http-client/src/index.ts +++ b/packages/http-client/src/index.ts @@ -147,7 +147,7 @@ export class HttpClient { handlers?: ifm.RequestHandler[], requestOptions?: ifm.RequestOptions ) { - this.userAgent = userAgent + this.userAgent = this._getUserAgentWithOrchestrationId(userAgent) this.handlers = handlers || [] this.requestOptions = requestOptions if (requestOptions) { @@ -816,6 +816,18 @@ export class HttpClient { return proxyAgent } + private _getUserAgentWithOrchestrationId(userAgent?: string): string { + const baseUserAgent = userAgent || 'actions/http-client' + const orchId = process.env['ACTIONS_ORCHESTRATION_ID'] + if (orchId) { + // Sanitize the orchestration ID to ensure it contains only valid characters + // Valid characters: 0-9, a-z, _, -, . + const sanitizedId = orchId.replace(/[^a-z0-9_.-]/gi, '_') + return `${baseUserAgent} actions_orchestration_id/${sanitizedId}` + } + return baseUserAgent + } + private async _performExponentialBackoff(retryNumber: number): Promise { retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber) const ms: number = ExponentialBackoffTimeSlice * Math.pow(2, retryNumber)