Skip to content

Commit 0dfbca9

Browse files
michaelmalaveheroku-johnnyheroku-devtools-release-workflows[bot]
authored
chore: merge in main (#348)
Co-authored-by: Johnny Winn <88165065+heroku-johnny@users.noreply.github.com> Co-authored-by: heroku-devtools-release-workflows[bot] <261039447+heroku-devtools-release-workflows[bot]@users.noreply.github.com>
1 parent cf0544a commit 0dfbca9

6 files changed

Lines changed: 75 additions & 6 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "12.3.1"
2+
".": "12.3.2"
33
}

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010

1111
* adopt @heroku-cli/test-utils and modernize test setup ([#308](https://github.com/heroku/heroku-cli-command/issues/308)) ([db7be8e](https://github.com/heroku/heroku-cli-command/commit/db7be8eb0e84422e40240c5df5d063513dbe528f))
1212

13+
## [12.3.2](https://github.com/heroku/heroku-cli-command/compare/command-v12.3.1...command-v12.3.2) (2026-04-29)
14+
15+
16+
### Bug Fixes
17+
18+
* dedupe API warning headers within one client invocation ([#343](https://github.com/heroku/heroku-cli-command/issues/343)) ([ac1717a](https://github.com/heroku/heroku-cli-command/commit/ac1717a69a86935a6e75f4c2bcbe77f35ec2e20c))
19+
1320
## [12.3.1](https://github.com/heroku/heroku-cli-command/compare/command-v12.2.2...command-v12.3.1) (2026-04-03)
1421

1522

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@heroku-cli/command",
33
"description": "base class for Heroku CLI commands",
4-
"version": "12.3.1",
4+
"version": "12.3.2",
55
"author": "Heroku",
66
"bugs": "https://github.com/heroku/heroku-cli-command/issues",
77
"dependencies": {

src/api-client.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export class APIClient {
9797
protocol: apiUrl.protocol,
9898
}
9999
const delinquencyConfig: IDelinquencyConfig = {fetch_delinquency: false, warning_shown: false}
100+
const shownHeaderWarnings = new Set<string>()
100101
this.http = class APIHTTPClient<T> extends HTTP.create(opts)<T> {
101102
static configDelinquency(url: string, opts: APIClient.Options): void {
102103
if (opts.method?.toUpperCase() !== 'GET' || (opts.hostname && opts.hostname !== apiUrl.hostname)) {
@@ -247,10 +248,17 @@ export class APIClient {
247248

248249
static showWarnings<T>(response: HTTP<T>) {
249250
const warnings = response.headers['x-heroku-warning'] || response.headers['warning-message']
251+
const emitIfNew = (raw: string) => {
252+
const normalized = raw.replace(/^\s*warning:?\s*/i, '').trim()
253+
if (!normalized || shownHeaderWarnings.has(normalized)) return
254+
shownHeaderWarnings.add(normalized)
255+
warn(normalized)
256+
}
257+
250258
if (Array.isArray(warnings))
251-
for (const warning of warnings) warn(warning.replace(/^\s*warning:?\s*/i, ''))
259+
for (const warning of warnings) emitIfNew(warning)
252260
else if (typeof warnings === 'string')
253-
warn(warnings.replace(/^\s*warning:?\s*/i, ''))
261+
emitIfNew(warnings)
254262
}
255263

256264
static trackRequestIds<T>(response: HTTP<T>) {

test/api-client.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,60 @@ describe('api_client', () => {
937937
expect(stderr.output).to.contain('Warning: some other warning')
938938
expect(stderr.output).not.to.contain('Warning: Warning: some other warning')
939939
})
940+
941+
test
942+
.it('does not repeat the same warning on subsequent identical responses', async ctx => {
943+
api = nock('https://api.heroku.com', {
944+
reqheaders: {authorization: 'Bearer mypass'},
945+
})
946+
api.get('/apps').twice().reply(200, [], {'X-Heroku-Warning': 'Your account password will expire soon.'})
947+
948+
const cmd = new Command([], ctx.config)
949+
stderr.start()
950+
await cmd.heroku.get('/apps')
951+
await cmd.heroku.get('/apps')
952+
stderr.stop()
953+
954+
expect(stderr.output.match(/Warning: Your account password will expire soon\./g)?.length).to.equal(1)
955+
})
956+
957+
test
958+
.it('shows distinct warnings from successive responses', async ctx => {
959+
api = nock('https://api.heroku.com', {
960+
reqheaders: {authorization: 'Bearer mypass'},
961+
})
962+
api.get('/apps').twice().reply(200, [], {'X-Heroku-Warning': 'First warning'})
963+
api.get('/apps/foo').reply(200, [], {'X-Heroku-Warning': 'Second warning'})
964+
965+
const cmd = new Command([], ctx.config)
966+
stderr.start()
967+
await cmd.heroku.get('/apps')
968+
await cmd.heroku.get('/apps')
969+
await cmd.heroku.get('/apps/foo')
970+
stderr.stop()
971+
972+
expect(stderr.output).to.contain('Warning: First warning')
973+
expect(stderr.output.match(/Warning: First warning/g)?.length).to.equal(1)
974+
expect(stderr.output).to.contain('Warning: Second warning')
975+
expect(stderr.output.match(/Warning: Second warning/g)?.length).to.equal(1)
976+
})
977+
978+
test
979+
.it('shows the same header warning again for a new command instance', async ctx => {
980+
api = nock('https://api.heroku.com', {
981+
reqheaders: {authorization: 'Bearer mypass'},
982+
})
983+
api.get('/apps').twice().reply(200, [], {'X-Heroku-Warning': 'Password expiry reminder'})
984+
985+
const cmd1 = new Command([], ctx.config)
986+
const cmd2 = new Command([], ctx.config)
987+
stderr.start()
988+
await cmd1.heroku.get('/apps')
989+
await cmd2.heroku.get('/apps')
990+
stderr.stop()
991+
992+
expect(stderr.output.match(/Warning: Password expiry reminder/g)?.length).to.equal(2)
993+
})
940994
})
941995

942996
context('with Warning-Message header set on response', function () {

0 commit comments

Comments
 (0)