d4rty
d4rty

Reputation: 4188

File Upload with Angular 6 towards a Flask (Python) Webservice

I created a webservice with flask to save files, which strongly follows the officially flask example:

@app.route('/parse_table', methods=['POST'])
def upload_file():
    print(request.files)
    # check if the post request has the file part
    if 'file' not in request.files:
        print('no file in request')
        return""
    file = request.files['file']
    if file.filename == '':
        print('no selected file')
        return""
    if file and allowed_file(file.filename):
        print("hello")
        filename = secure_filename(file.filename)
        file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
        return ""
    print("end")
    return""

When I simply using forms in my webapp the service works perfectly (file will be stored on server)

<form action = "http://127.0.0.1:5000/parse_table" method = "POST"
      enctype = "multipart/form-data">
  <input type = "file" name = "file" /        
  <input type = "submit"/>
</form>

When trying to handle the upload via Angular HttpClient, flask detects no file in request (i.e. print('no file in request') will be executed)

component.html

<input #fileInput name="file" type="file" (change)="handleFileInput($event.target.files)">
<button mat-raised-button class="standard-button" (click)="uploadFile()">Upload file</button>

component.ts

  @ViewChild('fileInput') fileInput;
  uploadFile() {
    const files: FileList = this.fileInput.nativeElement.files;
    if (files.length === 0) {
      return;
    };

    this.backendCommunicationService.parseTable(files).subscribe((data: any) => {
      console.log(data);
    });

backend-communication.service.ts

parseTable(files) {
  const httpOptions = {
    headers: new HttpHeaders({
        'Content-Type': 'multipart/form-data',
    })
  };
  const formData: FormData = new FormData();
  formData.append('file', files[0], files[0].name);
  return this.http.post(this.backendAddress + '/parse_table', formData, httpOptions)
}

Any suggestions/ hints?

Upvotes: 1

Views: 7769

Answers (2)

kartoon
kartoon

Reputation: 1194

If you are using one of multipart/* content types, you are actually required to specify the boundary parameter in the Content-Type header, otherwise the server (in the case of an HTTP request) will not be able to parse the payload.

From RFC2046:

The Content-Type field for multipart entities requires one parameter, "boundary". The boundary delimiter line is then defined as a line consisting entirely of two hyphen characters ("-", decimal value 45) followed by the boundary parameter value from the Content-Type header field, optional linear whitespace, and a terminating CRLF.

Boundary delimiters must not appear within the encapsulated material, and must be no longer than 70 characters, not counting the two leading hyphens.

The boundary delimiter line following the last body part is a distinguished delimiter that indicates that no further body parts will follow. Such a delimiter line is identical to the previous delimiter lines, with the addition of two more hyphens after the boundary parameter value.

Hence, your request should look like

Content-Type: multipart/form-data; --unique-boundary-1
Content-Disposition: form-data; name="file"; filename="download.jpg"

--unique-boundary-1

Look at the Content-Type and Form Data in the image below

enter image description here

In angular, best way to achieve this is by not including Content-Type in HttpHeaders. Let the browser add it for you based on your content.

httpOptions = {
    headers: new HttpHeaders({
        // 'Content-Type': 'multipart/form-data', // comment this out
        Authorization: this.userAuthContent
    })
};

HTML

<input type="file" placeholder="Select images" accept="image/*" multiple
          (change)="changeInFileInput($event.target.files)">

Typescript

changeInFileInput(files: any): void {
    if (files) {
        // iterate over all the files
        Array.from(files).forEach((fileData: File) => {
            // upload using promise
            this.uploadImage(fileData.file)
                .then((response: any) => {
                    // ...
                })
                .catch((error: any) => {
                    // ...
                });
        });
    }
}

uploadImage(fileToUpload: File): Promise < any > {
  return new Promise((resolve, reject) => {
    // create form data
    const formData: FormData = new FormData();
    // append image file to it
    formData.append('file', fileToUpload, fileToUpload.name);
    // attach formData (just created above) and httpOptions (created earlier) to post request
    this.httpClient.post(environment.bapiurls.uploadRefImage, formData, httpOptions)
      .subscribe(
        (response: any) => {
            resolve(response);
        },
        (error: any) => {
            reject(error);
        });
  });
}

Upvotes: 1

d4rty
d4rty

Reputation: 4188

Removing the httpOptions; e.g., the explicitly set http header, solves the problem. Comparing the HTTP post requests from angular webapp and from the html form showed, that in the http header Content-Type despite multipart/form-data' a boundary is needed, which will be not set by angular, when the http header is explicitly set. Removing this from the http post solves the isuue:

parseTable(files) {
  const formData: FormData = new FormData();
  formData.append('file', files[0], files[0].name);
  return this.http.post(this.backendAddress + '/parse_table', formData);
}

Upvotes: 4

Related Questions