2

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

Fabian Omobono
  • 81
  • 2
  • 13
  • I see that you're using `post` method in the form... that's good. Now when it comes to enable file uploads using forms in Django, one normally uses `enctype="multipart/form-data"`. Since this is not a normal form but more like an AJAX one, [as Quentin mentions](https://stackoverflow.com/a/18869351/5675325) you have to set the headers to `"Content-Type": "multipart/form-data"`. – Tiago Martins Peres Jul 23 '22 at 09:35
  • Can you print `request.FILES` to see if you have the file there? If you don't get anything, make sure you are setting `MEDIA_ROOT` and `MEDIA_URL` in `settings.py`. – Tiago Martins Peres Jul 23 '22 at 09:43
  • NOT manually setting `Content-Type` should resolve this error based from [here](https://stackoverflow.com/questions/39280438/fetch-missing-boundary-in-multipart-form-data-post) and the warning that is specified [here](https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects#sending_files_using_a_formdata_object) – Brian Destura Jul 25 '22 at 00:44

1 Answers1

0

As mentioned by @Brian Destura. Not specifying headers should work.

It is also worth mentioning that this logic path where no headers are specified is the only one included in the automated tests for graphene-file-upload.

Finally, the spec referenced has no example where a header required, or even included.

References

https://github.com/lmcgartland/graphene-file-upload/blob/master/tests/test_django.py

https://github.com/jaydenseric/graphql-multipart-request-spec

pygeek
  • 7,356
  • 1
  • 20
  • 41
  • removing the content type key from the header, or even the whole header from the fetch request did not work. I get this error back: Must provide query string – Fabian Omobono Jul 29 '22 at 18:06