I am trying to upload an image from my react/nextJS front end to my Django backend using graphQL and graphene-file-upload. Following this: https://github.com/lmcgartland/graphene-file-upload and this https://github.com/jaydenseric/graphql-multipart-request-spec
This is the frontend - the important functions here are handleSelectedLogo and submitVendor
const CreateVendorForm = () => {
// redux state
const user = useSelector(selectUser);
const token = useSelector(selectToken);
const [step, setStep] = useState(1)
const inputRef = useRef(null)
const [companyName, setCompanyName] = useState('')
const [companyAddress, setCompanyAddress] = useState<any>({label: ''})
const [companyLatLng, setCompanyLatLng] = useState<any>({lat: 40.014, lng: -105.270})
const [companyPhone, setCompanyPhone] = useState('')
const [companyEmail, setCompanyEmail] = useState('')
const [companyWebsite, setCompanyWebsite] = useState('')
const [companyLogo, setCompanyLogo] = useState()
const [companyDescription, setCompanyDescription] = useState('')
const [published, setPublished] = useState(false)
const [type, setType] = useState(['Customer Care', 'Information Technology','Market Research', 'Marketing and Communications', 'Renewable Energy', 'Technical Engineering', 'Other'])
const [selectedTypes , setSelectedTypes] = useState<string[]>([])
const [showtext, setShowText] = useState(false)
const [file , setFile] = useState()
useEffect(() => {
console.log(token,user)
}, [])
const toggleType = (type: string) => {
if (selectedTypes.includes(type)){
setSelectedTypes(selectedTypes.filter(t => t !== type))
}
else {
setSelectedTypes([...selectedTypes, type])
}
console.log(selectedTypes)
}
const changeStep = (nextStep: number) => {
if(nextStep > 0) {
if(step < 4) {
setStep(step + 1)
}
}
if (nextStep < 0) {
if (step > 1) {
setStep(step - 1)
}
}
}
const openUpload = () => {
inputRef.current.click()
}
const handleSelectedLogo = (e: any) => {
setCompanyLogo(URL.createObjectURL(e.target.files[0]))
const data = document.querySelector('#logo').files[0]
const f = new FormData()
f.append('file', data)
setFile(f)
}
const submitVendor = () => {
// send the data to Django
console.log('This is the file: ', file)
fetch(baseUrl, {
method: 'POST',
headers: {
'Authorization': `JWT ${token}`,
'Content-Type': 'multipart/form-data',
},
body: JSON.stringify({ query: `
mutation($file: Upload!) {
createLogo(file: $file) {
success
}
}
`,
variables: {
file: file
}
})
})
.then(res => res.json())
.then(data => console.log(data))
}
const displayStep = () => {
if (step === 1){
return (
<div className={styles.step}>
<section className={styles.basicInfo}>
<h2>Step 1: Basic Info</h2>
<div className={styles.companyLogo}>
<Image src={companyLogo ? companyLogo: macbook} width={677} height={300} layout='fixed' onClick={() => openUpload()} onMouseEnter={() => setShowText(true)} onMouseLeave={() => setShowText(false)}/>
{showtext && <div className={styles.logoText}><h2>Click on image to upload your company Logo</h2></div>}
<input ref={inputRef} id='logo' type='file' placeholder='Company Logo' onChange={e => handleSelectedLogo(e)} accept="image/png, image/gif, image/jpeg"/>
</div>
<div className={styles.nameAndDescription}>
<input type='text' placeholder='Company Name' value={companyName} onChange={e =>setCompanyName(e.target.value)}/>
<textarea placeholder='Company Description' value={companyDescription} onChange={e =>setCompanyDescription(e.target.value)} maxLength={500}/>
</div>
</section>
</div>
)
}
else if (step === 2){
return (
<div className={styles.step}>
<section className={styles.contactInfoContainer}>
<h2>Step 2: Add a location</h2>
<div >
<Map
companyName={companyName}
companyAddress={companyAddress}
setCompanyAddress={setCompanyAddress}
companyLatLng={companyLatLng}
setCompanyLatLng={setCompanyLatLng}
/>
</div>
</section>
</div>
)
}
else if (step === 3){
return (
<div className={styles.step}>
<h2>Step 3: Contact Info</h2>
<section className={styles.contactInfoInputs}>
<input type='phone' placeholder='Company Phone' value={companyPhone} onChange={e =>setCompanyPhone(e.target.value)}/>
<input type='email' placeholder='Company Email' value={companyEmail} onChange={e =>setCompanyEmail(e.target.value)}/>
<input type='text' placeholder='Company Website' value={companyWebsite} onChange={e =>setCompanyWebsite(e.target.value)}/>
</section>
</div>
)
}
else if (step === 4){
return (
<div className={styles.step}>
<section className={styles.companyType}>
<h2>Step 4: Company Type</h2>
{type.map(t =>
<div key={t}>
<input id={t} type='checkbox' value={t} onChange={e =>toggleType(e.target.value)}/>
<label htmlFor={t}>{t}</label>
</div>
)}
<h2>Status</h2>
<input type='radio' id='published' name='published' checked={published} onChange={e =>setPublished(true)} />
<label className={styles.publishedLabel} htmlFor='published'>Published</label>
<input type='radio' id='unpublished' name='published' checked={!published} onChange={e =>setPublished(false)} />
<label className={styles.publishedLabel} htmlFor='unpublished'>Unpublished</label>
</section>
</div>
)
}
}
return (
<div className={styles.createVendorFormOuterContainer}>
<div className={styles.header}>
<h1>Create a listing</h1>
</div>
<div className={styles.innerContainer}>
<aside className={styles.stepNumber}>
<p className={step === 1 ? styles.higlighted: ''} onClick={() => setStep(1)}>Step 1: Basic Info</p>
<p className={step === 2 ? styles.higlighted: ''} onClick={() => setStep(2)}>Step 2: Add a Location</p>
<p className={step === 3 ? styles.higlighted: ''} onClick={() => setStep(3)}>Step 3: Contact Info</p>
<p className={step === 4 ? styles.higlighted: ''} onClick={() => setStep(4)}>Step 4: Company type</p>
</aside>
<main>
{displayStep()}
<div className={styles.buttonNavigation}>
<button type='button' onClick={() => changeStep(-1)}>Previous Step</button>
<button type='button' onClick={() => changeStep(1)}>Next Step</button>
{step === 4 ? <button type='button' onClick={() => submitVendor()}>Submit</button> : null}
</div>
</main>
<aside className={styles.usefulSuggestions}>
<h2>Usefull Suggestions</h2>
<ul>
<li>Try to keep the description short and to the point</li>
<li>Try to include a picture of the product</li>
<li>Give you most recent address</li>
</ul>
</aside>
</div>
</div>
)
}
export default CreateVendorForm;
This is the backend schema.py
class CreateLogo(graphene.Mutation):
class Arguments:
file = Upload(required=True)
success = graphene.Boolean()
@staticmethod
def mutate(self, info, file, **kwargs):
# do something with your file
print(info.context.FILES.items)
print('This is file:', file)
print(kwargs)
logo = Logo(logo=file)
logo.save()
return CreateLogo(success=True)
class Mutation(graphene.ObjectType):
token_auth = graphql_jwt.ObtainJSONWebToken.Field()
verify_token = graphql_jwt.Verify.Field()
refresh_token = graphql_jwt.Refresh.Field()
#greetings
update_greeting = UpdateGreeting.Field()
create_greeting = CreateGreeting.Field()
#vendors
create_vendor = CreateVendor.Field()
update_vendor = UpdateVendor.Field()
single_upload = UploadMutation.Field()
create_logo = CreateLogo.Field()
schema = graphene.Schema(query=Query, mutation=Mutation)
This is the error I am getting
django.http.multipartparser.MultiPartParserError: Invalid boundary in multipart: None
[20/Jul/2022 00:21:47] "POST /api/graphql/ HTTP/1.1" 400 143
If I change the header of the fetch request to Content-type: "application/json" the requests succeed but the file argument in the mutate method is empty, and so is info.context.FILES.items
So to sum up: the fetch request fails when the header is Content-Type: multipart/form-data and succeeds, but no file is sent if the header is Content-Type: "application/json.
I searched the web for any similar problem, but it seems that no one has a solution to this problem.
My guess is that I need Django to accept the multipart/form-data header for the files to show up, but I am not sure where to fix that