BulkOperationRunMutation Error: Unexpected error - /tmp/2773680.../bulk/df1db455-627b-4732-8db6-50b48079308e/single_products_AO_4123sDWL.jsonl not valid for shop 2773680..., bucket bulk/

Hi there. I’m getting an error when trying to run a bulk mutation stating that my store doesn’t have access to the uploaded resource. The resource is being uploaded properly, returning a 201 status code. Additionally, when I check the url, I can view the data. However, it is stating that the store can’t access it despite the id’s being the same between the resourceUrl and the store. I’m programming in NodeJS. Here is some of my code:

async function stageFileUpload(client, jsonlPath) {
    const bulk_query = `
        mutation {
            stagedUploadsCreate(
                input:[{
                    resource: BULK_MUTATION_VARIABLES,
                    filename: "${jsonlPath}",
                    mimeType: "text/jsonl",
                    httpMethod: POST
                }]
            )
            {
                userErrors {
                    field,
                    message
                },
                stagedTargets {
                    url,
                    resourceUrl,
                    parameters {
                        name,
                        value
                    }
                }
            }
        }
    `

    try {
            console.log("Staging bulk request...");
            const data = await client.request(bulk_query);
            console.log("Bulk request staged successfully...");
            const form = new FormData();
            for(const item of data.data.stagedUploadsCreate.stagedTargets[0].parameters) {
                form.append(item.name, item.value)
            }
            form.append('file', fs.createReadStream(jsonlPath))
            console.log("Form prepared and file being uploaded...")

            //post request
            const res = await axios.post(data.data.stagedUploadsCreate.stagedTargets[0].url, form, {
                headers: form.getHeaders(),
                maxContentLength: Infinity,
                maxBodyLength: Infinity
            })
            console.log("File uploaded! Status:", res.status);
            return data.data.stagedUploadsCreate.stagedTargets[0];

        } catch (err) {
            console.error("Error adding products:", err.res?.errors || err.message);
            return 'error';
        }
}

async function addProductsAndVariants(client, staged_upload_path) {
    const bulk_query = `
        mutation { bulkOperationRunMutation(
                mutation: ${JSON.stringify("mutation ($input: ProductInput!) { productCreate(input: $input) { product { id } userErrors { field message } } }")},
                stagedUploadPath: ${JSON.stringify(new URL(staged_upload_path).pathname)}) {
                    bulkOperation {
                        id
                        url
                        status
                    }
                    userErrors {
                        message
                        field
                    }
                }
        }
    `
    try {
        console.log("Adding products...");
        const data = await client.request(bulk_query);
        console.log('bulk ran successfully');
        return data.data;
    } catch (err) {
        console.error("Error adding products:", err.message);
        return 'error';
    }
}

The jsonlpath is the path to the file (ex: products.jsonl). Both functions are being called with the same client passed. If anyone knows what the issue is, please let me know! Thanks.

1 Like

Hey @Adam_Heaney,

Looking here, the issue appears to be that the stagedUploadPath parameter is being constructed incorrectly. The “not valid for shop” error typically happens when you’re using the wrong value for the staged upload path in your bulkOperationRunMutation. The bulk operation actually expects the exact key parameter value from the stagedUploadsCreate response.

The main difference is that the URL path you are using looks to be including a leading slash. Instead, pass the key parameter from the parameters array of your stagedUploadsCreate response.

The bulk operations import documentation covers this flow in detail and shows how to properly reference the staged file.

Hi Kyle, thanks for the help. I forgot to include the part of code (in my main function) where the stagedUploadPath is correctly created using the key parameter, so I don’t think that was the problem.

Interestingly, I tried combining both of my functions into one function as shown below, and it worked:

async function uploadProducts (store) {
    const shopify = shopifyApi({
            apiVersion: ApiVersion.April25,
            isCustomStoreApp: true,
            isEmbeddedApp: false,
            adminApiAccessToken: store.accessToken,
            apiSecretKey: store.apiSecret,
            scopes: store.scopes,
            hostName: "localhost:3000",

            logger: {
            log: () => {},
            },
        });

        const client = new shopify.clients.Graphql({
            session: {
                shop: store.name + ".myshopify.com",
                accessToken: store.accessToken,
            },
        });

        const staged_upload_query = `
        mutation {
            stagedUploadsCreate(
                input:[{
                    resource: BULK_MUTATION_VARIABLES,
                    filename: "${JSONL_filepath}",
                    mimeType: "text/jsonl",
                    httpMethod: POST
                }]
            )
            {
                userErrors {
                    field,
                    message
                },
                stagedTargets {
                    url,
                    resourceUrl,
                    parameters {
                        name,
                        value
                    }
                }
            }
        }
    `

    let staged_upload_path;

    try {
        console.log("Staging upload request...");
        const data = await client.request(staged_upload_query);
        console.log("Upload request staged successfully...");
        const form = new FormData();
        for(const item of data.data.stagedUploadsCreate.stagedTargets[0].parameters) {
            form.append(item.name, item.value)
        }
        form.append('file', fs.createReadStream(JSONL_filepath))
        console.log("Form prepared and file being uploaded...")

        //post request
        const res = await axios.post(data.data.stagedUploadsCreate.stagedTargets[0].url, form, {
            headers: form.getHeaders(),
        })
        console.log("File uploaded! Status:", res.status);
        staged_upload_path = data.data.stagedUploadsCreate.stagedTargets[0];

    } catch (err) {
        console.error("Error uploading products:", err.res?.errors || err.message);
        return 'error';
    }
    
    const keyParam = staged_upload_path.parameters.find(p => p.name === 'key').value;
    staged_upload_path = staged_upload_path.resourceUrl + keyParam

    const bulk_query = `
        mutation { bulkOperationRunMutation(
                mutation: ${JSON.stringify("mutation ($input: ProductInput!) { productCreate(input: $input) { product { id title } userErrors { field message } } }")},
                stagedUploadPath: ${JSON.stringify(staged_upload_path)}) {
                    bulkOperation {
                        id
                        url
                        status
                    }
                    userErrors {
                        message
                        field
                    }
                }
        }
    `
    try {
        console.log("Adding products...");
        const data = await client.request(bulk_query);
        console.log('bulk running');
        return pollBulkOperationStatus(client);
    } catch (err) {
        console.error("Error running bulk:", err.message);
        return 'error';
    }
}

I didn’t make any changes to the logic, mutations, parameters, or processing. I simply combined the functions into a single function to ensure that the client was the exact same instance between the two steps and wasn’t being passed in as a parameter.

It seems like the problem was with the client variable and how it was being created and passed through to the functions, which is weird, since it was a single client object. Here is my main function from the previous code:

const JSONL_filepath = process.env.JSONL_FILEPATH
    
    let items = await getProductsInputByName();
    
    // fsPromises.writeFile('./test.json', JSON.stringify(items, null, 2))
    generateShopifyBulkCreateFile(items, JSONL_filepath);
    
    let staged_upload_path = await stageFileUpload(client, JSONL_filepath);
    staged_upload_path = staged_upload_path.resourceUrl + staged_upload_path.parameters[3].value
    console.log(staged_upload_path)
    const res = await addProductsAndVariants(client, staged_upload_path)
    console.log(res.bulkOperationRunMutation.userErrors)
    await pollBulkOperationStatus(client)

(parameter[3] is key, I checked [yes I know I should make it smarter to search for the parameter)

Thanks.

1 Like