Skip to content

Commit e1231e3

Browse files
authored
Merge pull request #164 from launchcodedev/swallowing-not-found-errors
Avoid swallowing transitive NotFoundError
2 parents 8e854db + bdea181 commit e1231e3

File tree

10 files changed

+111
-25
lines changed

10 files changed

+111
-25
lines changed

app-config-config/src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export async function loadUnvalidatedConfig({
8282
return { parsed, fullConfig: parsed.toJSON() };
8383
} catch (error) {
8484
// having no APP_CONFIG environment variable is normal, and should fall through to reading files
85-
if (!(error instanceof NotFoundError)) throw error;
85+
if (!NotFoundError.isNotFoundError(error)) throw error;
8686
}
8787

8888
const meta = await loadMetaConfig({ directory });
@@ -123,7 +123,7 @@ export async function loadUnvalidatedConfig({
123123
.read(secretsFileExtensions)
124124
.catch((error) => {
125125
// NOTE: secrets are optional, so not finding them is normal
126-
if (error instanceof NotFoundError) {
126+
if (NotFoundError.isNotFoundError(error, join(directory, secretsFileNameBase))) {
127127
logger.verbose('Did not find secrets file');
128128
return undefined;
129129
}
@@ -160,7 +160,7 @@ export async function loadUnvalidatedConfig({
160160
parsed = ParsedValue.merge(parsed, parsedExtension);
161161
} catch (error) {
162162
// having no APP_CONFIG_CI environment variable is normal, and should fall through to reading files
163-
if (!(error instanceof NotFoundError)) throw error;
163+
if (!NotFoundError.isNotFoundError(error)) throw error;
164164
}
165165
}
166166

app-config-core/src/config-source.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ import { parse as parseJSON5, stringify as stringifyJSON5 } from 'json5';
55
import { Json, JsonObject } from '@app-config/utils';
66
import { logger } from '@app-config/logging';
77
import { ParsedValue, ParsingContext, ParsingExtension } from './parsed-value';
8-
import { AppConfigError, NotFoundError, ParsingError, BadFileType } from './errors';
8+
import {
9+
AppConfigError,
10+
NotFoundError,
11+
ParsingError,
12+
BadFileType,
13+
FallbackExhaustedError,
14+
} from './errors';
915

1016
/**
1117
* File formats that app-config supports.
@@ -22,6 +28,8 @@ export enum FileType {
2228

2329
/** Base class for "sources", which are strategies to read configuration (eg. files, environment variables) */
2430
export abstract class ConfigSource {
31+
public readonly filePath?: string;
32+
2533
/** Only method that is *required* for all ConfigSources, which is built on in readValue, read, and readToJSON */
2634
protected abstract readContents(): Promise<[string, FileType]>;
2735

@@ -122,6 +130,8 @@ export class FallbackSource extends ConfigSource {
122130

123131
// override because readContents uses it (which is backwards from super class)
124132
async readValue(): Promise<Json> {
133+
const errors: NotFoundError[] = [];
134+
125135
// take the first value that comes back without an error
126136
for (const source of this.sources) {
127137
try {
@@ -130,19 +140,28 @@ export class FallbackSource extends ConfigSource {
130140

131141
return value;
132142
} catch (error) {
133-
if (error instanceof NotFoundError) {
143+
if (source.filePath) {
144+
// special case for ConfigSource with `filePath`, only accept a NotFoundError for it's filePath
145+
if (NotFoundError.isNotFoundError(error, source.filePath)) {
146+
errors.push(error);
147+
continue;
148+
}
149+
} else if (NotFoundError.isNotFoundError(error)) {
150+
errors.push(error);
134151
continue;
135152
}
136153

137154
throw error;
138155
}
139156
}
140157

141-
throw new NotFoundError('FallbackSource found no valid ConfigSource');
158+
throw new FallbackExhaustedError('FallbackSource found no valid ConfigSource', errors);
142159
}
143160

144161
// override so that ParsedValue is directly from the originating ConfigSource
145162
async read(extensions?: ParsingExtension[], context?: ParsingContext): Promise<ParsedValue> {
163+
const errors: NotFoundError[] = [];
164+
146165
// take the first value that comes back without an error
147166
for (const source of this.sources) {
148167
try {
@@ -151,15 +170,22 @@ export class FallbackSource extends ConfigSource {
151170

152171
return value;
153172
} catch (error) {
154-
if (error instanceof NotFoundError) {
173+
if (source.filePath) {
174+
// special case for ConfigSource with `filePath`, only accept a NotFoundError for it's filePath
175+
if (NotFoundError.isNotFoundError(error, source.filePath)) {
176+
errors.push(error);
177+
continue;
178+
}
179+
} else if (NotFoundError.isNotFoundError(error)) {
180+
errors.push(error);
155181
continue;
156182
}
157183

158184
throw error;
159185
}
160186
}
161187

162-
throw new NotFoundError('FallbackSource found no valid ConfigSource');
188+
throw new FallbackExhaustedError('FallbackSource found no valid ConfigSource', errors);
163189
}
164190
}
165191

app-config-core/src/errors.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,35 @@ export class AppConfigError extends Error {}
55
export class Fallbackable extends AppConfigError {}
66

77
/** When a ConfigSource cannot be found */
8-
export class NotFoundError extends Fallbackable {}
8+
export class NotFoundError extends Fallbackable {
9+
constructor(message?: string, public readonly filepath?: string) {
10+
super(message);
11+
}
12+
13+
static isNotFoundError(err: Error | unknown, filepath?: string): err is NotFoundError {
14+
if (err instanceof NotFoundError) {
15+
if (filepath) {
16+
return err.filepath === filepath;
17+
}
18+
19+
return true;
20+
}
21+
22+
return false;
23+
}
24+
}
25+
26+
export class EnvironmentVariableNotFoundError extends NotFoundError {
27+
constructor(message?: string, public readonly environmentVariable?: string) {
28+
super(message);
29+
}
30+
}
31+
32+
export class FallbackExhaustedError extends NotFoundError {
33+
constructor(message: string, public readonly errors: NotFoundError[]) {
34+
super(message);
35+
}
36+
}
937

1038
/** Could not parse a string from a config source */
1139
export class ParsingError extends AppConfigError {}

app-config-extensions/src/extends-directive.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function fileReferenceDirective(keyName: string, meta: ParsedValueMetadata): Par
5757
environmentOptions,
5858
})
5959
.catch((error) => {
60-
if (error instanceof NotFoundError && isOptional) {
60+
if (isOptional && NotFoundError.isNotFoundError(error, resolvedPath)) {
6161
return ParsedValue.literal({});
6262
}
6363

app-config-meta/src/index.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,19 @@ describe('meta file loading', () => {
160160
},
161161
);
162162
});
163+
164+
it('bubbles up and does not ignore a NotFoundError from transitively included files', async () => {
165+
await withTempFiles(
166+
{
167+
'.app-config.meta.yml': `
168+
$extends: a-missing-file.yml
169+
`,
170+
},
171+
async (inDir) => {
172+
await expect(loadMetaConfig({ directory: inDir('.') })).rejects.toThrow(
173+
`File ${inDir('a-missing-file.yml')} not found`,
174+
);
175+
},
176+
);
177+
});
163178
});

app-config-meta/src/index.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import {
66
ParsingExtension,
77
FallbackSource,
88
FileType,
9-
NotFoundError,
109
AppConfigError,
10+
FallbackExhaustedError,
1111
} from '@app-config/core';
1212
import { logger } from '@app-config/logging';
1313

@@ -87,9 +87,9 @@ export async function loadMetaConfig({
8787
}
8888

8989
// we try to find meta find in our CWD, but fallback to the workspace (git repo) root
90-
const sources = [new FlexibleFileSource(join(directory, fileNameBase))];
90+
const sources = [new FlexibleFileSource(join(resolve(directory), fileNameBase))];
9191

92-
if (workspaceRoot) {
92+
if (workspaceRoot && workspaceRoot !== directory) {
9393
sources.push(new FlexibleFileSource(join(workspaceRoot, fileNameBase)));
9494
}
9595

@@ -110,12 +110,18 @@ export async function loadMetaConfig({
110110
value,
111111
};
112112
} catch (error) {
113-
if (error instanceof NotFoundError) {
114-
logger.verbose(
115-
`Meta file was not found in ${directory} or workspace root (${workspaceRoot ?? 'none'})`,
116-
);
117-
118-
return { value: {} };
113+
if (error instanceof FallbackExhaustedError) {
114+
for (const { filepath } of error.errors) {
115+
if (sources.some((s) => s.filePath === filepath)) {
116+
logger.verbose(
117+
`Meta file was not found in ${directory} or workspace root (${
118+
workspaceRoot ?? 'none'
119+
})`,
120+
);
121+
122+
return { value: {} };
123+
}
124+
}
119125
}
120126

121127
throw error;

app-config-node/src/environment-source.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { guessFileType, ConfigSource, FileType, NotFoundError } from '@app-config/core';
1+
import {
2+
guessFileType,
3+
ConfigSource,
4+
FileType,
5+
EnvironmentVariableNotFoundError,
6+
} from '@app-config/core';
27
import { logger } from '@app-config/logging';
38

49
/** Read configuration from an environment variable */
@@ -11,7 +16,10 @@ export class EnvironmentSource extends ConfigSource {
1116
const value = process.env[this.variableName];
1217

1318
if (!value) {
14-
throw new NotFoundError(`Could not read the environment variable '${this.variableName}'`);
19+
throw new EnvironmentVariableNotFoundError(
20+
`Could not read the environment variable '${this.variableName}'`,
21+
this.variableName,
22+
);
1523
}
1624

1725
const inferredFileType = await guessFileType(value);

app-config-node/src/file-source.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class FileSource extends ConfigSource {
4040
return [content.toString('utf-8'), this.fileType];
4141
} catch (err: unknown) {
4242
if (err && typeof err === 'object' && (err as { code?: string | number }).code === 'ENOENT') {
43-
throw new NotFoundError(`File ${this.filePath} not found`);
43+
throw new NotFoundError(`File ${this.filePath} not found`, this.filePath);
4444
}
4545

4646
throw err;
@@ -50,7 +50,7 @@ export class FileSource extends ConfigSource {
5050

5151
/** Read configuration from a file, found via "glob-like" search (any file format, with support for environment specific files) */
5252
export class FlexibleFileSource extends ConfigSource {
53-
private readonly filePath: string;
53+
public readonly filePath: string;
5454
private readonly fileExtensions: string[];
5555
private readonly environmentOptions: EnvironmentOptions;
5656

@@ -138,6 +138,7 @@ export class FlexibleFileSource extends ConfigSource {
138138

139139
throw new NotFoundError(
140140
`FlexibleFileSource could not find file with ${this.filePath}.{yml|yaml|toml|json|json5}`,
141+
this.filePath,
141142
);
142143
}
143144

app-config-schema/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export async function loadSchema({
6262

6363
parsed = await env.read(parsingExtensions).catch((error) => {
6464
// having no APP_CONFIG_SCHEMA environment variable is normal, and should fall through to reading files
65-
if (error instanceof NotFoundError) {
65+
if (NotFoundError.isNotFoundError(error)) {
6666
return undefined;
6767
}
6868

app-config-settings/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ export async function loadSettings(): Promise<Settings> {
4545

4646
return value;
4747
} catch (error) {
48-
if (error instanceof NotFoundError) return {};
48+
if (NotFoundError.isNotFoundError(error, path)) {
49+
return {};
50+
}
4951

5052
throw error;
5153
}

0 commit comments

Comments
 (0)