Skip to content

Commit a057085

Browse files
committed
make throw
1 parent e88e326 commit a057085

File tree

10 files changed

+151
-27
lines changed

10 files changed

+151
-27
lines changed

.projenrc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,7 @@ const cli = configureProject(
10081008
'jest-environment-node',
10091009
'jest-mock',
10101010
'madge',
1011+
'nock@13',
10111012
'sinon',
10121013
'ts-mock-imports',
10131014
'xml-js',

packages/@aws-cdk/toolkit-lib/lib/api/notices/cached-data-soure.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as fs from 'fs-extra';
22
import type { Notice, NoticeDataSource } from './types';
33
import type { IoDefaultMessages } from '../io/private';
4+
import { ToolkitError } from '../toolkit-error';
45

56
interface CachedNotices {
67
expiration: number;
@@ -25,28 +26,32 @@ export class CachedDataSource implements NoticeDataSource {
2526
const expiration = cachedData.expiration ?? 0;
2627

2728
if (Date.now() > expiration || this.skipCache) {
28-
const freshData = await this.fetchInner();
29-
await this.save(freshData);
30-
return freshData.notices;
29+
let updatedData: CachedNotices = cachedData;
30+
31+
try {
32+
updatedData = await this.fetchInner();
33+
} catch (e) {
34+
this.ioMessages.debug(`Could not refresh notices: ${e}`);
35+
updatedData = {
36+
expiration: Date.now() + TIME_TO_LIVE_ERROR,
37+
notices: [],
38+
};
39+
throw ToolkitError.withCause('Failed to load CDK notices. Please try again later.', e);
40+
} finally {
41+
await this.save(updatedData);
42+
}
43+
return updatedData.notices;
3144
} else {
3245
this.ioMessages.debug(`Reading cached notices from ${this.fileName}`);
3346
return data;
3447
}
3548
}
3649

3750
private async fetchInner(): Promise<CachedNotices> {
38-
try {
39-
return {
40-
expiration: Date.now() + TIME_TO_LIVE_SUCCESS,
41-
notices: await this.dataSource.fetch(),
42-
};
43-
} catch (e) {
44-
this.ioMessages.debug(`Could not refresh notices: ${e}`);
45-
return {
46-
expiration: Date.now() + TIME_TO_LIVE_ERROR,
47-
notices: [],
48-
};
49-
}
51+
return {
52+
expiration: Date.now() + TIME_TO_LIVE_SUCCESS,
53+
notices: await this.dataSource.fetch(),
54+
};
5055
}
5156

5257
private async load(): Promise<CachedNotices> {

packages/@aws-cdk/toolkit-lib/lib/api/notices/web-data-source.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import type { ClientRequest } from 'http';
22
import type { RequestOptions } from 'https';
33
import * as https from 'node:https';
4-
import { formatErrorMessage } from '../../util';
54
import type { SdkHttpOptions } from '../aws-auth';
65
import { ProxyAgentProvider } from '../aws-auth/private';
76
import type { IoHelper } from '../io/private';
87
import { IO } from '../io/private';
98
import { ToolkitError } from '../toolkit-error';
109
import type { Notice, NoticeDataSource } from './types';
10+
import { formatErrorMessage, humanHttpStatusError, humanNetworkError } from '../../util';
1111

1212
export class WebsiteNoticeDataSource implements NoticeDataSource {
1313
private readonly options: SdkHttpOptions;
@@ -48,23 +48,25 @@ export class WebsiteNoticeDataSource implements NoticeDataSource {
4848
try {
4949
const data = JSON.parse(rawData).notices as Notice[];
5050
if (!data) {
51-
throw new ToolkitError("'notices' key is missing");
51+
throw new ToolkitError("'notices' key is missing from received data");
5252
}
5353
resolve(data ?? []);
5454
} catch (e: any) {
55-
reject(new ToolkitError(`Failed to parse notices: ${formatErrorMessage(e)}`));
55+
reject(ToolkitError.withCause(formatErrorMessage(e), e));
5656
}
5757
});
5858
res.on('error', e => {
59-
reject(new ToolkitError(`Failed to fetch notices: ${formatErrorMessage(e)}`));
59+
reject(ToolkitError.withCause(formatErrorMessage(e), e));
6060
});
6161
} else {
62-
reject(new ToolkitError(`Failed to fetch notices. Status code: ${res.statusCode}`));
62+
reject(new ToolkitError(`${humanHttpStatusError(res.statusCode!)} (Status code: ${res.statusCode})`));
6363
}
6464
});
65-
req.on('error', reject);
65+
req.on('error', e => {
66+
reject(ToolkitError.withCause(humanNetworkError(e), e));
67+
});
6668
} catch (e: any) {
67-
reject(new ToolkitError(`HTTPS 'get' call threw an error: ${formatErrorMessage(e)}`));
69+
reject(ToolkitError.withCause(formatErrorMessage(e), e));
6870
}
6971
});
7072

packages/@aws-cdk/toolkit-lib/lib/util/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from './content-hash';
77
export * from './directories';
88
export * from './format-error';
99
export * from './json';
10+
export * from './net';
1011
export * from './objects';
1112
export * from './parallel';
1213
export * from './package-info';
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Get a human-readable error message for a network error
3+
* @param error The network error object
4+
*/
5+
export function humanNetworkError(error: NodeJS.ErrnoException): string {
6+
switch (error.code) {
7+
case 'ENOTFOUND':
8+
return `Cannot reach the server. Please check your internet connection or the URL (${(error as any).hostname}).`;
9+
case 'ECONNREFUSED':
10+
return `Connection refused. The server at ${(error as any).address}:${(error as any).port} is not accepting connections.`;
11+
case 'ECONNRESET':
12+
return 'Connection was suddenly closed by the server. Please try again later.';
13+
case 'ETIMEDOUT':
14+
return 'Connection timed out. The server took too long to respond.';
15+
case 'CERT_HAS_EXPIRED':
16+
return 'The SSL certificate of the server has expired. This could be a security risk.';
17+
case 'UNABLE_TO_VERIFY_LEAF_SIGNATURE':
18+
case 'CERT_SIGNATURE_FAILURE':
19+
case 'ERR_TLS_CERT_ALTNAME_INVALID':
20+
return 'SSL certificate validation failed. This could indicate a security issue or a misconfigured server.';
21+
default:
22+
return `Network error: ${error.message || error.code || 'Unknown error'}`;
23+
}
24+
}
25+
26+
/**
27+
* Get a human-readable error message for a HTTP status code
28+
*/
29+
export function humanHttpStatusError(statusCode: number): string {
30+
switch (statusCode) {
31+
case 400:
32+
return 'Bad request - the server could not understand the request';
33+
case 401:
34+
return 'Unauthorized - authentication is required';
35+
case 403:
36+
return 'Forbidden - you do not have permission to access this resource';
37+
case 404:
38+
return 'Not found - the requested resource does not exist';
39+
case 408:
40+
return 'Request timeout - the server timed out waiting for the request';
41+
case 429:
42+
return 'Too many requests - you have sent too many requests in a given amount of time';
43+
case 500:
44+
return 'Internal server error - something went wrong on the server';
45+
case 502:
46+
return 'Bad gateway - the server received an invalid response from an upstream server';
47+
case 503:
48+
return 'Service unavailable - the server is temporarily unable to handle the request';
49+
case 504:
50+
return 'Gateway timeout - the server did not receive a timely response from an upstream server';
51+
default:
52+
return statusCode >= 500
53+
? 'Server error - something went wrong on the server'
54+
: 'Client error - something went wrong with the request';
55+
}
56+
}

packages/@aws-cdk/toolkit-lib/test/api/notices.test.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,7 @@ describe(WebsiteNoticeDataSource, () => {
605605
await expect(result).rejects.toThrow(/timed out/);
606606
});
607607

608-
test('returns empty array when the request takes too long to finish', async () => {
608+
test('returns appropriate error when the request takes too long to finish', async () => {
609609
nock('https://cli.cdk.dev-tools.aws.dev')
610610
.get('/notices.json')
611611
.delayBody(3500)
@@ -808,15 +808,17 @@ describe(Notices, () => {
808808
ioHost.expectMessage({ containing: 'There are 0 unacknowledged notice(s).' });
809809
});
810810

811-
test('doesnt throw', async () => {
811+
test('re-throws error from data source', async () => {
812812
const notices = Notices.create({ ioHost, context: new Context(), cliVersion });
813-
await notices.refresh({
813+
const res = notices.refresh({
814814
dataSource: {
815815
fetch: async () => {
816-
throw new Error('Should not fail refresh');
816+
throw new Error('Should fail refresh');
817817
},
818818
},
819819
});
820+
821+
await expect(res).rejects.toThrow('Should fail refresh');
820822
});
821823

822824
test('filters out acknowledged notices by default', async () => {

packages/aws-cdk/.projen/deps.json

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

packages/aws-cdk/lib/cli/pretty-print-error.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export function prettyPrintError(error: unknown, debug = false) {
99
if (err.cause) {
1010
const cause = ensureError(err.cause);
1111
console.error(chalk.yellow(cause.message));
12-
printTrace(err, debug);
12+
printTrace(cause, debug);
1313
}
1414

1515
printTrace(err, debug);

packages/aws-cdk/package.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import * as nock from 'nock';
2+
import { exec } from '../../lib/cli/cli';
3+
4+
const BASIC_NOTICE = {
5+
title: 'Toggling off auto_delete_objects for Bucket empties the bucket',
6+
issueNumber: 16603,
7+
overview:
8+
'If a stack is deployed with an S3 bucket with auto_delete_objects=True, and then re-deployed with auto_delete_objects=False, all the objects in the bucket will be deleted.',
9+
components: [
10+
{
11+
name: 'cli',
12+
version: '<=1.126.0',
13+
},
14+
],
15+
schemaVersion: '1',
16+
};
17+
18+
beforeEach(() => {
19+
jest.clearAllMocks();
20+
});
21+
22+
describe('cdk notices', () => {
23+
test('will fail on dns error', async () => {
24+
// GIVEN
25+
nock('https://cli.cdk.dev-tools.aws.dev')
26+
.get('/notices.json')
27+
.replyWithError('DNS resolution failed');
28+
29+
// WHEN
30+
const res = exec(['notices']);
31+
32+
// THEN
33+
await expect(res).rejects.toThrow('DNS resolution failed');
34+
});
35+
36+
test('will fail on timeout', async () => {
37+
// GIVEN
38+
nock('https://cli.cdk.dev-tools.aws.dev')
39+
.get('/notices.json')
40+
.delayConnection(3500)
41+
.reply(200, {
42+
notices: [BASIC_NOTICE],
43+
});
44+
45+
// WHEN
46+
const res = exec(['notices']);
47+
48+
// THEN
49+
await expect(res).rejects.toThrow('Request timed out');
50+
});
51+
});

0 commit comments

Comments
 (0)