Loading Errors

CORS, Untrusted Assets and Loading Errors

The loader fetches with XMLHttpRequest, so a model or texture on another origin loads only if that server sends Access-Control-Allow-Origin, and a .gltf file's separate .bin and images are checked again. Failures arrive as promise rejections (this page ran from a local HTTP server):

Catching loading errors, and validating a suspicious fileJavaScript
for (const url of ['missing.glb', 'https://example.com/books.glb']) {  // 404, then no CORS
  await BABYLON.LoadAssetContainerAsync(url, scene).catch((error) =>
    console.log(error.message.replace('Unable to load from ', '').split('  - ')[0]));
}
const broken = { asset: { version: '2.0' },                   // an accessor that does not exist
  meshes: [{ primitives: [{ attributes: { POSITION: 7 } }] }] };
const report = await new Promise((resolve) => BABYLON.LoadAssetContainerAsync(
  'data:' + JSON.stringify(broken), scene,                    // the load itself succeeds
  { pluginOptions: { gltf: { validate: true, onValidated: resolve } } }));
const [first] = report.issues.messages;
console.log(`${report.issues.numErrors} error: ${first.code} at ${first.pointer}`);
Output
missing.glb: File not found
https://example.com/books.glb: LoadFileError: Error status: 0
1 error: UNRESOLVED_REFERENCE at /meshes/0/primitives/0/attributes/POSITION

A CORS refusal reaches script only as status 0 (the reason is in the console), after Babylon 89,603 's default retry strategy has tried three more times. Uploaded or purchased glTF files run no script but can exhaust memory or point URIs at other hosts. validate: true runs Khronos's glTF-Validator but does not hold the load back (the broken file loaded anyway; the report came later), so to refuse bad files validate first, ideally on the server (gltf-validator on npm 2,036 ), check sizes, rewrite URIs with preprocessUrlAsync, and serve uploads from a separate origin under a Content Security Policy.

Three loads handled as promises: a good GLB, a glTF with a missing accessor and a truncated fileHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
  canvas { width: 100%; max-width: 600px; height: 330px; display: block; touch-action: none; outline: none; }
</style>
<canvas id="view"></canvas>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script src="https://cdn.jsdelivr.net/npm/babylonjs-gui@9.28.0/babylon.gui.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/babylonjs-loaders@9.28.0/babylonjs.loaders.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/babylonjs-serializers@9.28.0/babylonjs.serializers.min.js"></script>
<script>
// ---- BookNest's bookstore: the helpers the book keeps in bookstore.js ----
const ROOM = { width: 8, depth: 6, height: 3, wall: 0.15 };
const BOOKS = [
  { id: 1, title: 'The Quiet Harbor', author: 'Mara Ellison', color: '#1f5f8b', pages: 312, price: 14.99, rating: 4.6, inStock: true },
  { id: 2, title: 'Patterns of the Deep Web', author: 'Tomas Reyes', color: '#5b3f99', pages: 428, price: 32.5, rating: 4.4, inStock: true },
  { id: 3, title: 'Salt and Saffron', author: 'Priya Nair', color: '#e09a10', pages: 256, price: 24, rating: 4.8, inStock: false },
  { id: 4, title: 'Small Steps to Big Summits', author: 'Dana Whitfield', color: '#3f7d3a', pages: 198, price: 18.5, rating: 4.3, inStock: true },
  { id: 5, title: "The Clockmaker's Paradox", author: 'Julian Voss', color: '#b5452f', pages: 344, price: 16.75, rating: 4.7, inStock: true },
  { id: 6, title: 'Gardens in Glass', author: 'Lena Hart', color: '#2a9d8f', pages: 176, price: 21.3, rating: 4.5, inStock: true },
];
function createScene(engine) {
  const scene = new BABYLON.Scene(engine);
  scene.clearColor = BABYLON.Color4.FromHexString('#cfd8dcff');
  const sky = new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.4), scene);
  sky.groundColor = new BABYLON.Color3(0.35, 0.32, 0.3);
  return scene;
}
function paint(scene, name, hex) {                  // a matte color
  const material = new BABYLON.StandardMaterial(name, scene);
  material.diffuseColor = BABYLON.Color3.FromHexString(hex);
  material.specularColor = BABYLON.Color3.Black();
  return material;
}
function createRoom(scene) {                        // floor and three walls, front open
  const room = new BABYLON.TransformNode('room', scene);
  const floor = BABYLON.MeshBuilder.CreateGround('floor', { width: ROOM.width, height: ROOM.depth }, scene);
  floor.material = paint(scene, 'oak', '#b08d6a');
  floor.parent = room;
  const plaster = paint(scene, 'plaster', '#fff3e0');
  for (const [name, width, x, z] of [['backWall', 8, 0, 3], ['leftWall', 6, -4, 0], ['rightWall', 6, 4, 0]]) {
    const wall = BABYLON.MeshBuilder.CreateBox(name, { width, height: ROOM.height, depth: ROOM.wall }, scene);
    wall.position.set(x, ROOM.height / 2, z);
    wall.rotation.y = x ? Math.PI / 2 : 0;
    wall.material = plaster;
    wall.parent = room;
  }
  return room;
}
function createBookcase(scene, name, x) {           // 8 boxes merged into 1 mesh
  const [w, h, d, t] = [3, 2.2, 0.35, 0.04];
  const parts = [[t, h, d, -w / 2, h / 2, 0], [t, h, d, w / 2, h / 2, 0], [w, h, t, 0, h / 2, d / 2 - t / 2]];
  for (let i = 0; i < 5; i++) parts.push([w, t, d, 0, 0.1 + i * 0.5, 0]);
  const bookcase = BABYLON.Mesh.MergeMeshes(parts.map(([width, height, depth, px, py, pz]) => {
    const part = BABYLON.MeshBuilder.CreateBox('part', { width, height, depth }, scene);
    part.position.set(px, py, pz);
    return part;
  }), true);
  bookcase.name = name;
  bookcase.material = paint(scene, 'walnut', '#6d4c33');
  bookcase.position.set(x, 0, ROOM.depth / 2 - ROOM.wall / 2 - d / 2);   // against the back wall
  return bookcase;
}
function drawCover(scene, book) {                   // cover art painted on a canvas
  const tex = new BABYLON.DynamicTexture('cover' + book.id, { width: 256, height: 384 }, scene);
  const c = tex.getContext();
  c.fillStyle = book.color; c.fillRect(0, 0, 256, 384);
  c.fillStyle = 'rgba(0,0,0,0.4)'; c.fillRect(0, 0, 18, 384);         // the dark left edge
  c.strokeStyle = '#fff8e1'; c.lineWidth = 3; c.strokeRect(34, 24, 202, 336);
  c.fillStyle = '#fff8e1'; c.textAlign = 'center'; c.font = 'bold 30px Georgia';
  const lines = [''];
  for (const word of book.title.split(' ')) {
    const test = (lines[lines.length - 1] + ' ' + word).trim();
    if (c.measureText(test).width > 180) lines.push(word); else lines[lines.length - 1] = test;
  }
  lines.forEach((line, i) => c.fillText(line, 135, 110 + i * 38));
  c.font = '20px Georgia'; c.fillText(book.author, 135, 330);
  tex.update();
  return tex;
}
function placeBooks(scene, bookcase, shelf = 2) {   // six books, spines out, covers on +x
  const strip = new BABYLON.Vector4(0, 0, 0.07, 1);
  const faceUV = [strip, strip, new BABYLON.Vector4(0, 0, 1, 1), strip, strip, strip];
  let x = -0.35;                                    // left end of the row, local to the bookcase
  return BOOKS.map((record) => {
    const width = record.pages / 4000 + 0.02;       // spine thickness from page count
    const book = BABYLON.MeshBuilder.CreateBox(`book${record.id}`, { width, height: 0.3, depth: 0.22, faceUV, wrap: true }, scene);
    book.parent = bookcase;
    book.position.set(x + width / 2, 0.12 + shelf * 0.5 + 0.15, -0.02);
    x += width + 0.012;
    book.material = new BABYLON.StandardMaterial(record.title, scene);
    book.material.diffuseTexture = drawCover(scene, record);
    book.material.specularColor = new BABYLON.Color3(0.15, 0.15, 0.15);
    book.metadata = record;                         // the record, for picking
    return book;
  });
}
// The book cuts the storefront with CSG2 (Manifold WebAssembly); this sandbox cannot download
// the .wasm, so the older pure-JavaScript BABYLON.CSG makes the same cuts here.
function createFrontWall(scene) {
  const box = (width, height, depth, x, y) => {
    const mesh = BABYLON.MeshBuilder.CreateBox('cut', { width, height, depth }, scene);
    mesh.position.set(x, y, -ROOM.depth / 2);
    return mesh;
  };
  const solid = box(ROOM.width, ROOM.height, ROOM.wall, 0, ROOM.height / 2);
  const windowHole = box(3.2, 1.6, 1, -1.4, 1.6), doorHole = box(1.1, 2.2, 1, 2.3, 1.1);
  const wall = BABYLON.CSG.FromMesh(solid).subtract(BABYLON.CSG.FromMesh(windowHole))
    .subtract(BABYLON.CSG.FromMesh(doorHole)).toMesh('frontWall', scene.getMaterialByName('plaster'), scene);
  [solid, windowHole, doorHole].forEach((mesh) => mesh.dispose());
  return wall;
}
// ---- end of the bookstore helpers ----

// The book loads booknest-books.glb over HTTP. This sandboxed preview cannot fetch files, so it
// first writes the same GLB in memory (six books with PBR covers and their records as glTF
// extras) with the serializers package, then hands those bytes to the loader.
async function createBooksGlb(engine) {
  const scratch = new BABYLON.Scene(engine);
  const shelf = new BABYLON.TransformNode('shelf', scratch);     // a parent that is not exported
  const books = placeBooks(scratch, shelf, 0);                  // shelf 0: books stand at y 0.27
  books.forEach((book) => {                                     // PBR covers -> glTF materials
    const cover = new BABYLON.PBRMaterial(book.material.name, scratch);
    Object.assign(cover, { albedoTexture: book.material.diffuseTexture, metallic: 0, roughness: 0.5 });
    book.material = cover;
  });
  await scratch.whenReadyAsync();
  const glb = await BABYLON.GLTF2Export.GLBAsync(scratch, 'booknest-books', {
    shouldExportNode: (node) => node.name.startsWith('book'),   // the six books only
    metadataSelector: (metadata) => metadata,                   // BOOKS[i] -> glTF extras
  });
  const bytes = new Uint8Array(await glb.files['booknest-books.glb'].arrayBuffer());
  scratch.dispose();
  return bytes;                                                 // pass with { pluginExtension: '.glb' }
}

/*
  Network failures (a 404, or another origin without Access-Control-Allow-Origin) reject the same
  promise. This sandbox blocks every request, so the demo feeds the loader bytes it made itself:
  one valid GLB and two broken files, and shows what each promise settles with.
*/
const canvas = document.getElementById('view');
const engine = new BABYLON.Engine(canvas, true, {}, true);
const scene = createScene(engine);
scene.clearColor = BABYLON.Color4.FromHexString('#f7f3ecff');
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2, 1.25, 4.2, new BABYLON.Vector3(0, 0.3, 0), scene);
camera.attachControl(canvas, true);
const ui = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI('ui');
const table = BABYLON.MeshBuilder.CreateGround('table', { width: 4.8, height: 1.6 }, scene);
table.material = paint(scene, 'table', '#e6ddd0');
engine.runRenderLoop(() => scene.render());

function pedestal(x, text, color) {
  const base = BABYLON.MeshBuilder.CreateCylinder('base', { height: 0.1, diameter: 0.8 }, scene);
  base.position.set(x, 0.05, 0);
  base.material = paint(scene, color, color);
  const label = new BABYLON.GUI.TextBlock('', text);
  Object.assign(label, { color: '#333', fontSize: 11, width: '150px', height: '60px', textWrapping: true });
  ui.addControl(label);
  label.linkWithMesh(base);
  label.linkOffsetY = 60;
  return base;
}
async function tryLoad(name, source, x) {
  try {
    const pluginExtension = name.slice(name.lastIndexOf('.'));   // '.glb' or '.gltf'
    const container = await BABYLON.LoadAssetContainerAsync(source, scene, { pluginExtension });
    container.addAllToScene();
    container.meshes[0].position.set(x + 0.35, 0.1, 0);
    pedestal(x, `${name}\nloaded: ${container.meshes.length - 1} meshes`, '#3f7d3a');
  } catch (error) {                                   // every failure arrives as a rejection
    const cross = BABYLON.MeshBuilder.CreateTorus('failed', { diameter: 0.4, thickness: 0.06 }, scene);
    cross.position.set(x, 0.4, 0);
    cross.rotation.x = Math.PI / 2;
    cross.material = paint(scene, 'red', '#b5452f');
    const message = error.message.split(': ').pop().slice(0, 60);   // drop "Unable to load from ..."
    pedestal(x, `${name}\nrejected: ${message}`, '#b5452f');
  }
}
(async () => {
  const good = await createBooksGlb(engine);
  await tryLoad('booknest-books.glb', good, -1.5);
  // A glTF whose primitive points at accessor 7, which does not exist
  const broken = { asset: { version: '2.0' }, meshes: [{ primitives: [{ attributes: { POSITION: 7 } }] }],
    nodes: [{ mesh: 0 }], scenes: [{ nodes: [0] }] };
  await tryLoad('broken.gltf', 'data:' + JSON.stringify(broken), 0);   // inline JSON, no request
  await tryLoad('truncated.glb', good.slice(0, 2000), 1.5);   // the download cut off
})();
addEventListener('resize', () => engine.resize());
</script>