Skip to content

Commit be93909

Browse files
Return null from loadTexture when image fails to load instead of wrongly creating a VideoTexture (#5781)
* Return null from loadTexture when image fails to load intead of wrongly creating a VideoTexture * Also add tests for video not found * Still use checkIsImageFallback in case of 3xx redirects or server not supporting HEAD requests * We can assume server not supporting HEAD request that it's not a video, revert the previous logic * Revert "Also add tests for video not found" This reverts commit b7867d0. * Remove third param * Update comments
1 parent 9ef58ed commit be93909

File tree

3 files changed

+66
-10
lines changed

3 files changed

+66
-10
lines changed

src/systems/material.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,14 @@ export var System = registerSystem('material', {
3030
*
3131
* @param {string|Element} src - URL or element
3232
* @param {object} data - Relevant texture properties
33-
* @param {function} cb - Callback to pass texture to
33+
* @param {function} cb - Callback that receives the texture, or null if image loading failed
3434
*/
3535
loadTexture: function (src, data, cb) {
3636
this.loadTextureSource(src, function sourceLoaded (source) {
37+
if (source === null) {
38+
cb(null);
39+
return;
40+
}
3741
var texture = createCompatibleTexture(source);
3842
setTextureProperties(texture, data);
3943
cb(texture);
@@ -44,15 +48,15 @@ export var System = registerSystem('material', {
4448
* Determine whether `src` is an image or video. Then try to load the asset, then call back.
4549
*
4650
* @param {string|Element} src - URL or element.
47-
* @param {function} cb - Callback to pass texture source to.
51+
* @param {function} cb - Callback that receives the texture source, or null if image loading failed.
4852
*/
4953
loadTextureSource: function (src, cb) {
5054
var self = this;
5155
var sourceCache = this.sourceCache;
5256

5357
var hash = this.hash(src);
5458
if (sourceCache[hash]) {
55-
sourceCache[hash].then(cb);
59+
sourceCache[hash].then(cb, function () { cb(null); });
5660
return;
5761
}
5862

@@ -71,7 +75,7 @@ export var System = registerSystem('material', {
7175

7276
function sourceLoaded (sourcePromise) {
7377
sourceCache[hash] = Promise.resolve(sourcePromise);
74-
sourceCache[hash].then(cb);
78+
sourceCache[hash].then(cb, function () { cb(null); });
7579
}
7680
},
7781

@@ -199,8 +203,9 @@ function loadImageUrl (src) {
199203
resolveSource,
200204
function () { /* no-op */ },
201205
function (xhr) {
202-
error('`$s` could not be fetched (Error code: %s; Response: %s)', xhr.status,
206+
error('`%s` could not be fetched (Error code: %s; Response: %s)', src, xhr.status,
203207
xhr.statusText);
208+
reject(new Error('Failed to load image: ' + src));
204209
}
205210
);
206211

src/utils/src-loader.js

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,17 @@ export function parseUrl (src) {
116116
return parsedSrc[1];
117117
}
118118

119+
var IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'avif'];
120+
121+
/**
122+
* Get file extension from URL (without query string or hash).
123+
*/
124+
function getExtension (src) {
125+
var pathname = src.split('?')[0].split('#')[0];
126+
var ext = pathname.split('.').pop().toLowerCase();
127+
return ext;
128+
}
129+
119130
/**
120131
* Call back whether `src` is an image.
121132
*
@@ -124,11 +135,20 @@ export function parseUrl (src) {
124135
*/
125136
function checkIsImage (src, onResult) {
126137
var request;
138+
var ext;
127139

128140
if (src.tagName) {
129141
onResult(src.tagName === 'IMG');
130142
return;
131143
}
144+
145+
// Check file extension first to avoid HEAD request for common image extensions.
146+
ext = getExtension(src);
147+
if (IMAGE_EXTENSIONS.indexOf(ext) !== -1) {
148+
onResult(true);
149+
return;
150+
}
151+
132152
request = new XMLHttpRequest();
133153

134154
// Try to send HEAD request to check if image first.
@@ -139,21 +159,31 @@ function checkIsImage (src, onResult) {
139159
contentType = request.getResponseHeader('Content-Type');
140160
if (contentType == null) {
141161
checkIsImageFallback(src, onResult);
162+
} else if (contentType.startsWith('image')) {
163+
onResult(true);
142164
} else {
143-
if (contentType.startsWith('image')) {
144-
onResult(true);
145-
} else {
146-
onResult(false);
147-
}
165+
onResult(false);
148166
}
149167
} else {
168+
// Non-success status (3xx redirects, 404, 405, etc.) - try loading via Image tag
169+
// as it handles redirects and the server might not support HEAD requests.
150170
checkIsImageFallback(src, onResult);
151171
}
152172
request.abort();
153173
});
174+
request.addEventListener('error', function () {
175+
// Network error (CORS, etc.) - try loading via Image tag.
176+
checkIsImageFallback(src, onResult);
177+
});
154178
request.send();
155179
}
156180

181+
/**
182+
* Try loading src as an image to determine if it's an image.
183+
*
184+
* @param {string} src - URL to test.
185+
* @param {function} onResult - Callback with result.
186+
*/
157187
function checkIsImageFallback (src, onResult) {
158188
var tester = new Image();
159189
tester.addEventListener('load', onLoad);

tests/systems/material.test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { entityFactory } from '../helpers.js';
33

44
var IMAGE1 = 'base/tests/assets/test.png';
55
var IMAGE2 = 'base/tests/assets/test2.png';
6+
var IMAGE_FAIL = 'base/tests/assets/nonexistent.png';
67
var VIDEO1 = 'base/tests/assets/test.mp4';
78
var VIDEO2 = 'base/tests/assets/test2.mp4';
89

@@ -122,6 +123,26 @@ suite('material system', function () {
122123
done();
123124
});
124125
});
126+
127+
test('returns null when image fails to load', function (done) {
128+
var system = this.system;
129+
130+
system.loadTextureSource(IMAGE_FAIL, function (source) {
131+
assert.equal(source, null);
132+
done();
133+
});
134+
});
135+
});
136+
137+
suite('loadTexture', function () {
138+
test('returns null when image fails to load', function (done) {
139+
var system = this.system;
140+
141+
system.loadTexture(IMAGE_FAIL, {}, function (texture) {
142+
assert.equal(texture, null);
143+
done();
144+
});
145+
});
125146
});
126147

127148
suite('loadVideo', function () {

0 commit comments

Comments
 (0)