Cannot upload .glb 3d model via graphql

Topic summary

A developer is encountering multiple issues when attempting to upload .glb 3D model files via Shopify’s GraphQL API (2023-07), while video and image uploads work without problems.

Primary Issues Identified:

  • Extension Mismatch Error: The fileCreate mutation returns “Provided filename extension must match original source” despite identical .glb extensions in both filename and originalSource fields. The developer suspects URL parameters in the originalSource may be causing the validation to fail.

  • Processing Failure: When the filename field is removed, the upload appears successful initially but remains in processing state for several minutes before ultimately failing.

  • Resource Type Configuration Bug: The core issue appears to be setting resource to ‘MODEL_3D’ (as documentation suggests) doesn’t work. Setting it to ‘FILE’ in stagedUploadsCreate generates a working URL, though this approach seems counterintuitive and poorly documented.

Workaround Found:

  • Use resource: "FILE" instead of resource: "MODEL_3D" during the staged upload process
  • The 3D model may only attach properly when linked to a product using productCreateMedia

Additional Context:

  • The same 3D model uploads and processes immediately via Shopify Admin UI
  • Another developer reports similar authentication/key errors when attempting the upload process
  • The developer notes Shopify’s internal upload system is “very strange and intolerant” with inadequate error messaging
Summarized with AI on November 11. AI used: claude-sonnet-4-5-20250929.
var file_inputs = [];
    var upload_inputs = [];
    var contents = [];
    for (var data of datas) {
      var filename = path.basename(data.filename);
      var stat = null;
      var resource = data.resource || ShopifyWrapper.UPLOAD_TARGET_RESOURCE_TYPES.FILE;
      try { stat = fs.statSync(data.filename); } catch {}
      var mimetype = data.mimetype || mime.lookup(data.filename) || "";
      var content_type = mimetype.split("/")[0].toUpperCase();
      if (!["FILE","IMAGE","VIDEO"].includes(content_type)) content_type = "FILE";
      var file_input = {
        "filename": filename,
        "alt": data.alt,
        "contentType": content_type,
      }
      var content = data.content || (await fs.readFile(data.filename));
      if (content instanceof Readable) content = await streamToBuffer(content);
      var size = data.size;
      if (!size && content) content.length
      var upload_input = {
        "filename": filename,
        "mimeType": mimetype,
        "resource": resource,
        "fileSize": size,
        "httpMethod": "POST",
      }

      contents.push(content);
      file_inputs.push(file_input);
      upload_inputs.push(upload_input);
    }
    if (upload_inputs.length) {
      var response = await this.graphql(
        `mutation stagedUploadsCreate($input: [StagedUploadInput!]!) {
          stagedUploadsCreate(input: $input) {
            stagedTargets {
              url
              resourceUrl
              parameters { name, value }
            }
            userErrors {
              field, message
            }
          }
        }`, {
          input: upload_inputs
        }
      );
      if (response.stagedUploadsCreate.userErrors.length) console.error(response.stagedUploadsCreate.userErrors);

      for (var i = 0; i < file_inputs.length; i++) {
        var file_input = file_inputs[i];
        var content = contents[i];
        var t = response.stagedUploadsCreate.stagedTargets[i];
        file_input.originalSource = t.resourceUrl;
        var form_data = new FormData();
        for (var p of t.parameters) {
          form_data.append(p.name, p.value);
        }
        form_data.append("file", content, {filename:file_input.filename});
        var res = await axios.post(t.url, form_data);
      }
    }
    return file_inputs;

^ This is the code I used. The bit you probably need to know about is how to define each StagedUploadInput:

var upload_input = {
        "filename": filename,
        "mimeType": mimetype,
        "resource": resource,
        "fileSize": size,
        "httpMethod": "POST",
      }

You use that to define each file you’re about to upload and it has to be correct. For 3d models (or any file type) the ‘resource’ field has to equal “FILE” if you are just uploading to the ‘Files’ section essentially. It’s a very strange and intolerant system!
The response contains a special url that you can then upload to. Then you make a request to that url for each file using the httpMethod that you stated earlier, attaching each file as form data.