Skip to content

Commit c9f3976

Browse files
devversioncrisbeto
authored andcommitted
fix(core): properly recognize failed fetch responses when loading external resources in JIT (#62992)
Currently when loading external resources in JIT, when `fetch` fails, the `text` is empty and the component is loading. This hides the actual underlying fetch error. We should properly detect this and error out. PR Close #62992
1 parent 8cd7166 commit c9f3976

File tree

4 files changed

+40
-5
lines changed

4 files changed

+40
-5
lines changed

goldens/public-api/core/errors.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export const enum RuntimeErrorCode {
4242
// (undocumented)
4343
EXPRESSION_CHANGED_AFTER_CHECKED = -100,
4444
// (undocumented)
45+
EXTERNAL_RESOURCE_LOADING_FAILED = 918,
46+
// (undocumented)
4547
HOST_DIRECTIVE_COMPONENT = 310,
4648
// (undocumented)
4749
HOST_DIRECTIVE_CONFLICTING_ALIAS = 312,

packages/core/src/errors.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export const enum RuntimeErrorCode {
126126
MISSING_NG_MODULE_DEFINITION = 915,
127127
MISSING_DIRECTIVE_DEFINITION = 916,
128128
NO_COMPONENT_FACTORY_FOUND = 917,
129+
EXTERNAL_RESOURCE_LOADING_FAILED = 918,
129130

130131
// Signal integration errors
131132
REQUIRED_INPUT_NO_VALUE = -950,
@@ -143,7 +144,7 @@ export const enum RuntimeErrorCode {
143144
RUNTIME_DEPS_INVALID_IMPORTED_TYPE = 980,
144145
RUNTIME_DEPS_ORPHAN_COMPONENT = 981,
145146

146-
// Resource errors
147+
// resource() API errors
147148
MUST_PROVIDE_STREAM_OPTION = 990,
148149
RESOURCE_COMPLETED_BEFORE_PRODUCING_VALUE = 991,
149150

packages/core/src/metadata/resource_loading.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import {RuntimeError, RuntimeErrorCode} from '../errors';
910
import {Type} from '../interface/type';
1011

1112
import type {Component} from './directives';
@@ -43,7 +44,7 @@ import type {Component} from './directives';
4344
* contents of the resolved URL. Browser's `fetch()` method is a good default implementation.
4445
*/
4546
export function resolveComponentResources(
46-
resourceResolver: (url: string) => Promise<string | {text(): Promise<string>}>,
47+
resourceResolver: (url: string) => Promise<string | {text(): Promise<string>; status?: number}>,
4748
): Promise<void> {
4849
// Store all promises which are fetching the resources.
4950
const componentResolved: Promise<void>[] = [];
@@ -54,7 +55,7 @@ export function resolveComponentResources(
5455
let promise = urlMap.get(url);
5556
if (!promise) {
5657
const resp = resourceResolver(url);
57-
urlMap.set(url, (promise = resp.then(unwrapResponse)));
58+
urlMap.set(url, (promise = resp.then((res) => unwrapResponse(url, res))));
5859
}
5960
return promise;
6061
}
@@ -147,8 +148,22 @@ export function isComponentResourceResolutionQueueEmpty() {
147148
return componentResourceResolutionQueue.size === 0;
148149
}
149150

150-
function unwrapResponse(response: string | {text(): Promise<string>}): string | Promise<string> {
151-
return typeof response == 'string' ? response : response.text();
151+
function unwrapResponse(
152+
url: string,
153+
response: string | {text(): Promise<string>; status?: number},
154+
): string | Promise<string> {
155+
if (typeof response === 'string') {
156+
return response;
157+
}
158+
if (response.status !== undefined && response.status !== 200) {
159+
return Promise.reject(
160+
new RuntimeError(
161+
RuntimeErrorCode.EXTERNAL_RESOURCE_LOADING_FAILED,
162+
ngDevMode && `Could not load resource: ${url}. Response status: ${response.status}`,
163+
),
164+
);
165+
}
166+
return response.text();
152167
}
153168

154169
function componentDefResolved(type: Type<any>): void {

packages/core/test/metadata/resource_loading_spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,5 +198,22 @@ Did you run and wait for 'resolveComponentResources()'?`.trim(),
198198
expect(MyComponent.ɵcmp).toBeDefined();
199199
expect(metadata.template).toBe('response for test://content');
200200
});
201+
202+
it('should fail when fetch is resolving to a 404', async () => {
203+
const MyComponent: ComponentType<any> = class MyComponent {} as any;
204+
const metadata: Component = {templateUrl: 'test://content'};
205+
compileComponent(MyComponent, metadata);
206+
207+
await expectAsync(
208+
resolveComponentResources(async () => {
209+
return {
210+
async text() {
211+
return 'File not found';
212+
},
213+
status: 404,
214+
};
215+
}),
216+
).toBeRejectedWithError(/Could not load resource.*404/);
217+
});
201218
});
202219
});

0 commit comments

Comments
 (0)