A space to discuss GraphQL queries, mutations, troubleshooting, throttling, and best practices.
Hey, I'm trying to upload the media with graphql. I read all the docs and this article:
https://shopify.dev/docs/apps/online-store/media/products#step-1-upload-media-to-shopify .
There's my file.server.js:
export const uploadFile = async (graphql) => { const result = await graphql(` mutation generateStagedUploads { stagedUploadsCreate(input: [ { filename: "watches_comparison.mp4", mimeType: "video/mp4", resource: VIDEO, fileSize: "899765" }, { filename: "another_watch.glb", mimeType: "model/gltf-binary", resource: MODEL_3D, fileSize: "456" } ]) { stagedTargets { url resourceUrl parameters { name value } } userErrors { field, message } } } `); console.log('result', result); console.log('result.data', result.data); console.log('result.data?.stagedUploadsCreate', result.data?.stagedUploadsCreate); console.log('result.data?.stagedUploadsCreate.stagedTargets', result.data?.stagedUploadsCreate.stagedTargets); return result; }
And I call it in the loader:
const { admin } = await authenticate.admin(request); await uploadFile(admin.graphql)
This response returns right data, when i use it in Shopify GraphiQL App.
But when i do it on the server, i have an empty object in response. What am I doing wrong?
Solved! Go to the solution
This is an accepted solution.
Ok, this code works. I hope it will be usefull:
// sopify.app.toml
scopes = "read_products,write_products,write_files,read_files,read_themes,write_themes"
// components/filesUploader.jsx
const handleDropZoneDrop = useCallback(async (_dropFiles, acceptedFiles, _rejectedFiles) => {
const formData = new FormData();
if (acceptedFiles.length) {
setFiles(acceptedFiles);
}
acceptedFiles.forEach((file) => {
formData.append('files', file)
});
const result = await fetch('/file', {
'method': 'POST',
headers: {
contentType: 'multipart/form-data',
},
body: formData,
});
}, []);
// routes/file.jsx
import { unstable_parseMultipartFormData, unstable_createMemoryUploadHandler } from "@remix-run/node";
import { authenticate } from "~/shopify.server";
import { uploadFile } from "~/models/file.server";
export const action = async ({request}) => {
const { admin } = await authenticate.admin(request);
const uploadHandler = unstable_createMemoryUploadHandler({
maxPartSize: 20_000_000,
});
const formData = await unstable_parseMultipartFormData(
request,
uploadHandler
);
const files = formData.getAll('files');
const result = await uploadFile(files, admin.graphql);
return {
data: result,
}
}
// files.server.js
import {fetch, FormData} from "@remix-run/node";
const prepareFiles = (files) => files.map((file) => ({
filename: file.name,
mimeType: file.type,
resource: file.type.includes('image') ? 'IMAGE' : 'FILE',
fileSize: file.size.toString(),
httpMethod: 'POST',
}));
const prepareFilesToCreate = (stagedTargets, files, contentType) => stagedTargets.map((stagedTarget, index) => {
return {
originalSource: stagedTarget.resourceUrl,
contentType: files[index].type.includes('image') ? 'IMAGE' : 'FILE',
filename: files[index].name,
};
});
export const uploadFile = async (files, graphql) => {
const preparedFiles = prepareFiles(files);
const result = await graphql(`
mutation stagedUploadsCreate($input: [StagedUploadInput!]!) {
stagedUploadsCreate(input: $input) {
stagedTargets {
resourceUrl
url
parameters {
name
value
}
}
userErrors {
field,
message
}
}
}
`, { variables: { input: preparedFiles }});
const response = await result.json();
const promises = [];
files.forEach((file, index) => {
const url = response.data.stagedUploadsCreate.stagedTargets[index].url;
const params = response.data.stagedUploadsCreate.stagedTargets[index].parameters;
const formData = new FormData();
params.forEach((param) => {
formData.append(param.name, param.value)
})
formData.append('file', file);
const promise = fetch(url, {
method: 'POST',
body: formData,
});
promises.push(promise);
});
await Promise.all(promises);
await graphql(`
mutation fileCreate($files: [FileCreateInput!]!) {
fileCreate(files: $files) {
files {
id,
preview {
image {
url
}
}
}
userErrors {
field
message
}
}
}
`, {
variables: {
files: prepareFilesToCreate(response.data.stagedUploadsCreate.stagedTargets, files),
}
});
return {
stagedTargets: response.data.stagedUploadsCreate.stagedTargets,
errors: response.data.stagedUploadsCreate.userErrors
}
}
Hi EvilGranny,
It's possible that there's something happening here that's related to permissions or authentication. With the GraphiQL app, scopes and auth is already set up so since it's working with this method, it's likely your app isn't set up 100% correctly for it's scopes and auth. Just to confirm, does your app the `read_products` and `write_products` access scopes?
Liam | Developer Advocate @ Shopify
- Was my reply helpful? Click Like to let me know!
- Was your question answered? Mark it as an Accepted Solution
- To learn more visit Shopify.dev or the Shopify Web Design and Development Blog
Hey, thank you for your attention to my topic!
here's my scope:
[access_scopes]
# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes
scopes = "read_products,write_products,write_files,read_files"
When I didn't have some permissions, I've got an error, but now only empty object
Is there any way you can test the mutation itself (eg by using something like Postman) to rule out if it's the mutation that's causing this vs if the app configuration is causing this?
Liam | Developer Advocate @ Shopify
- Was my reply helpful? Click Like to let me know!
- Was your question answered? Mark it as an Accepted Solution
- To learn more visit Shopify.dev or the Shopify Web Design and Development Blog
I'd like to use postman, but I have no idea what I need to put in the headers, and which endpoint I should use ((
There's my shopify config, did I miss something
# Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration
name = "shop-reviews"
client_id = "b4d0dc605b0d03899187a08ec64fb24f"
application_url = "https://household-kruger-forms-blond.trycloudflare.com"
embedded = true
[access_scopes]
# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes
scopes = "read_products,write_products,read_files,write_files"
[auth]
redirect_urls = [
"https://household-kruger-forms-blond.trycloudflare.com/auth/callback",
"https://household-kruger-forms-blond.trycloudflare.com/auth/shopify/callback",
"https://household-kruger-forms-blond.trycloudflare.com/api/auth/callback"
]
[webhooks]
api_version = "2023-07"
[pos]
embedded = false
[build]
automatically_update_urls_on_dev = true
dev_store_url = "mmg-development-store.myshopify.com"
And here's response:
00:20:58 │ remix │ result NodeResponse [Response] {
00:20:58 │ remix │ size: 0,
00:20:58 │ remix │ [Symbol(Body internals)]: {
00:20:58 │ remix │ body: ReadableStream {
00:20:58 │ remix │ _state: 'readable',
00:20:58 │ remix │ _reader: undefined,
00:20:58 │ remix │ _storedError: undefined,
00:20:58 │ remix │ _disturbed: false,
00:20:58 │ remix │ _readableStreamController: [ReadableStreamDefaultController]
00:20:58 │ remix │ },
00:20:58 │ remix │ type: 'text/plain;charset=UTF-8',
00:20:58 │ remix │ size: 2436,
00:20:58 │ remix │ boundary: null,
00:20:58 │ remix │ disturbed: false,
00:20:58 │ remix │ error: null
00:20:58 │ remix │ },
00:20:58 │ remix │ [Symbol(Response internals)]: {
00:20:58 │ remix │ url: undefined,
00:20:58 │ remix │ status: 200,
00:20:58 │ remix │ statusText: '',
00:20:58 │ remix │ headers: {
00:20:58 │ remix │ 'alt-svc': 'h3=":443"; ma=86400',
00:20:58 │ remix │ 'cf-cache-status': 'DYNAMIC',
00:20:58 │ remix │ 'cf-ray': '817bee8bbc143515-WAW',
00:20:58 │ remix │ connection: 'close',
00:20:58 │ remix │ 'content-language': 'en',
00:20:58 │ remix │ 'content-security-policy': "default-src 'self' data: blob: 'unsafe-inline' 'unsafe-eval' https://* shopify-pos://*; block-all-mixed-content; child-src 'self' https://*
shopify-pos://*; connect-src 'self' wss://* https://*; frame-ancestors 'none'; img-src 'self' data: blob: https:; script-src https://cdn.shopify.com https://cdn.shopifycdn.net
https://checkout.shopifycs.com https://api.stripe.com https://mpsnare.iesnare.com https://appcenter.intuit.com https://www.paypal.com https://js.braintreegateway.com https://c.paypal.com
https://maps.googleapis.com https://www.google-analytics.com https://v.shopify.com 'self' 'unsafe-inline' 'unsafe-eval'; upgrade-insecure-requests; report-uri
/csp-report?source%5Baction%5D=query&source%5Bapp%5D=Shopify&source%5Bcontroller%5D=admin%2Fgraphql&source%5Bsection%5D=admin_api&source%5Buuid%5D=36b53ab3-532b-404c-9613-e1202d8b750a",
00:20:58 │ remix │ 'content-type': 'application/json; charset=utf-8',
00:20:58 │ remix │ date: 'Tue, 17 Oct 2023 22:20:58 GMT',
00:20:58 │ remix │ nel: '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}',
00:20:58 │ remix │ 'referrer-policy': 'origin-when-cross-origin',
00:20:58 │ remix │ 'report-to': '{"endpoints":[{"url":"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=U%2BnjgGzqnsUgTLlqdB78%2BP0lWiwU1MybCVj2sM%2Fk68dBTI9HUAx0tq2LZTjYY4xoiedpKaG7T7L6qw
mYyvPGQcGG1HOxE8%2FrwBr%2BqkJk2jssGUuWbZD7aAV7QLWRi3FGhQAI7%2BMqB%2F3R7MpP6Ofj1BP54KfR"}],"group":"cf-nel","max_age":604800}',
00:20:58 │ remix │ server: 'cloudflare',
00:20:58 │ remix │ 'server-timing': 'processing;dur=486, graphql;desc="admin/mutation/other", cfRequestDuration;dur=636.999846',
00:20:58 │ remix │ 'strict-transport-security': 'max-age=7889238',
00:20:58 │ remix │ 'transfer-encoding': 'chunked',
00:20:58 │ remix │ vary: 'Accept-Encoding',
00:20:58 │ remix │ 'x-content-type-options': 'nosniff',
00:20:58 │ remix │ 'x-dc': 'gcp-europe-west3,gcp-europe-west3,gcp-us-central1,gcp-us-central1',
00:20:58 │ remix │ 'x-download-options': 'noopen',
00:20:58 │ remix │ 'x-frame-options': 'DENY',
00:20:58 │ remix │ 'x-permitted-cross-domain-policies': 'none',
00:20:58 │ remix │ 'x-request-id': '36b53ab3-532b-404c-9613-e1202d8b750a',
00:20:58 │ remix │ 'x-shardid': '300',
00:20:58 │ remix │ 'x-shopid': '79825731885',
00:20:58 │ remix │ 'x-shopify-api-version': '2023-07',
00:20:58 │ remix │ 'x-shopify-stage': 'production',
00:20:58 │ remix │ 'x-sorting-hat-podid': '300',
00:20:58 │ remix │ 'x-sorting-hat-shopid': '79825731885',
00:20:58 │ remix │ 'x-stats-apiclientid': '66100330497',
00:20:58 │ remix │ 'x-stats-apipermissionid': '656712794413',
00:20:58 │ remix │ 'x-stats-userid': '',
00:20:58 │ remix │ 'x-xss-protection': '1; mode=block;
report=/xss-report?source%5Baction%5D=query&source%5Bapp%5D=Shopify&source%5Bcontroller%5D=admin%2Fgraphql&source%5Bsection%5D=admin_api&source%5Buuid%5D=36b53ab3-532b-404c-9613-e1202d8b750a'
00:20:58 │ remix │ },
00:20:58 │ remix │ counter: 0,
00:20:58 │ remix │ highWaterMark: undefined
00:20:58 │ remix │ }
00:20:58 │ remix │ }
Hi @EvilGranny,
If you want to use Postman to make HTTP requests while testing your app you can format them like these examples from our documentation on using access tokens.
If your app uses OAuth then you will need to use the session token that was issued when you installed the app on the shop.
All the best!
- James
Developer Support @ Shopify
- Was this reply helpful? Click Like to let us know!
- Was your question answered? Mark it as an Accepted Solution
- To learn more visit Shopify.dev or the Shopify Web Design and Development Blog
Thanks! I have no idea how, but I've fifxed it. But now I have a second problem.
Here's my code:
const prepareFiles = (files) => files.map(file => ({
filename: file.name,
mimeType: file.type,
resource: 'IMAGE',
fileSize: file.size.toString(),
}));
const prepareFilesToCreate = (stagedTargets, files) => stagedTargets.map((stagedTarget, index) => {
return {
originalSource: stagedTarget.resourceUrl,
contentType: 'IMAGE',
filename: files[index].name,
};
});
export const uploadFile = async (files, graphql) => {
const preparedFiles = prepareFiles(files);
const result = await graphql(`
mutation stagedUploadsCreate($input: [StagedUploadInput!]!) {
stagedUploadsCreate(input: $input) {
stagedTargets {
resourceUrl
parameters {
name
value
}
}
userErrors {
field,
message
}
}
}
`, { variables: { input: preparedFiles }});
const response = await result.json();
const filesSS = await graphql(`
mutation fileCreate($files: [FileCreateInput!]!) {
fileCreate(files: $files) {
files {
id,
preview {
image {
url
}
}
}
userErrors {
field
message
}
}
}
`, {
variables: {
files: prepareFilesToCreate(response.data.stagedUploadsCreate.stagedTargets, files),
}
});
const ll = await filesSS.json()
console.log(1111, ll.data.fileCreate.files)
console.log(5555, ll.data.fileCreate.userErrors)
return {
stagedTargets: response.data.stagedUploadsCreate.stagedTargets,
errors: response.data.stagedUploadsCreate.userErrors
}
}
And here's console.log() results:
image is null.
I tried to use different file formats, different sizes and so on...
On the screenshot result of jpg 400kb.
And when I go to content=>files I see errors. What am I doing wrong?
I've found the solution. This code works, but not for video files. When I'm uploading video files, I have an error: "No content"
import {fetch, FormData} from "@remix-run/node";
const prepareFiles = (files) => files.map((file) => ({
filename: file.name,
mimeType: file.type,
resource: file.type.includes('image') ? 'IMAGE' : 'VIDEO',
fileSize: file.size.toString(),
httpMethod: 'POST',
}));
const prepareFilesToCreate = (stagedTargets, files, contentType) => stagedTargets.map((stagedTarget, index) => {
return {
originalSource: stagedTarget.resourceUrl,
contentType: files[index].type.includes('image') ? 'IMAGE' : 'VIDEO',
filename: files[index].name,
};
});
export const uploadFile = async (files, graphql) => {
const preparedFiles = prepareFiles(files);
const result = await graphql(`
mutation stagedUploadsCreate($input: [StagedUploadInput!]!) {
stagedUploadsCreate(input: $input) {
stagedTargets {
resourceUrl
url
parameters {
name
value
}
}
userErrors {
field,
message
}
}
}
`, { variables: { input: preparedFiles }});
const response = await result.json();
const promises = [];
files.forEach((file, index) => {
const url = response.data.stagedUploadsCreate.stagedTargets[index].url;
const params = response.data.stagedUploadsCreate.stagedTargets[index].parameters;
const formData = new FormData();
params.forEach((param) => {
formData.append(param.name, param.value)
})
formData.append('file', file);
const promise = fetch(url, {
method: 'POST',
body: formData,
});
promises.push(promise);
});
await Promise.all(promises);
await graphql(`
mutation fileCreate($files: [FileCreateInput!]!) {
fileCreate(files: $files) {
files {
id,
preview {
image {
url
}
}
}
userErrors {
field
message
}
}
}
`, {
variables: {
files: prepareFilesToCreate(response.data.stagedUploadsCreate.stagedTargets, files),
}
});
return {
stagedTargets: response.data.stagedUploadsCreate.stagedTargets,
errors: response.data.stagedUploadsCreate.userErrors
}
}
This is an accepted solution.
Ok, this code works. I hope it will be usefull:
// sopify.app.toml
scopes = "read_products,write_products,write_files,read_files,read_themes,write_themes"
// components/filesUploader.jsx
const handleDropZoneDrop = useCallback(async (_dropFiles, acceptedFiles, _rejectedFiles) => {
const formData = new FormData();
if (acceptedFiles.length) {
setFiles(acceptedFiles);
}
acceptedFiles.forEach((file) => {
formData.append('files', file)
});
const result = await fetch('/file', {
'method': 'POST',
headers: {
contentType: 'multipart/form-data',
},
body: formData,
});
}, []);
// routes/file.jsx
import { unstable_parseMultipartFormData, unstable_createMemoryUploadHandler } from "@remix-run/node";
import { authenticate } from "~/shopify.server";
import { uploadFile } from "~/models/file.server";
export const action = async ({request}) => {
const { admin } = await authenticate.admin(request);
const uploadHandler = unstable_createMemoryUploadHandler({
maxPartSize: 20_000_000,
});
const formData = await unstable_parseMultipartFormData(
request,
uploadHandler
);
const files = formData.getAll('files');
const result = await uploadFile(files, admin.graphql);
return {
data: result,
}
}
// files.server.js
import {fetch, FormData} from "@remix-run/node";
const prepareFiles = (files) => files.map((file) => ({
filename: file.name,
mimeType: file.type,
resource: file.type.includes('image') ? 'IMAGE' : 'FILE',
fileSize: file.size.toString(),
httpMethod: 'POST',
}));
const prepareFilesToCreate = (stagedTargets, files, contentType) => stagedTargets.map((stagedTarget, index) => {
return {
originalSource: stagedTarget.resourceUrl,
contentType: files[index].type.includes('image') ? 'IMAGE' : 'FILE',
filename: files[index].name,
};
});
export const uploadFile = async (files, graphql) => {
const preparedFiles = prepareFiles(files);
const result = await graphql(`
mutation stagedUploadsCreate($input: [StagedUploadInput!]!) {
stagedUploadsCreate(input: $input) {
stagedTargets {
resourceUrl
url
parameters {
name
value
}
}
userErrors {
field,
message
}
}
}
`, { variables: { input: preparedFiles }});
const response = await result.json();
const promises = [];
files.forEach((file, index) => {
const url = response.data.stagedUploadsCreate.stagedTargets[index].url;
const params = response.data.stagedUploadsCreate.stagedTargets[index].parameters;
const formData = new FormData();
params.forEach((param) => {
formData.append(param.name, param.value)
})
formData.append('file', file);
const promise = fetch(url, {
method: 'POST',
body: formData,
});
promises.push(promise);
});
await Promise.all(promises);
await graphql(`
mutation fileCreate($files: [FileCreateInput!]!) {
fileCreate(files: $files) {
files {
id,
preview {
image {
url
}
}
}
userErrors {
field
message
}
}
}
`, {
variables: {
files: prepareFilesToCreate(response.data.stagedUploadsCreate.stagedTargets, files),
}
});
return {
stagedTargets: response.data.stagedUploadsCreate.stagedTargets,
errors: response.data.stagedUploadsCreate.userErrors
}
}