当前请求不是Angular 上的多部分请求,但在 Postman 中工作

bfnvny8b  于 2022-10-23  发布在  Spring
关注(0)|答案(2)|浏览(221)

我有一个工作上传后端与 Postman 。但我不能让它在Angular 上工作。

后台代码

package com.demo.web.api.file;

import static java.nio.file.Files.copy;
import static java.nio.file.Paths.get;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import com.esotericsoftware.minlog.Log;

@RestController
@RequestMapping(value = "/files")
public class FileUploadService {

    @PostMapping(value = "/upload", consumes = MediaType.ALL_VALUE)
    public ResponseEntity<List<String>> uploadFiles(@RequestParam("files") MultipartFile[] files) {

        Log.info("Processing file upload...");

        List<String> exceptions = new ArrayList<>();

        // Upload directory
        final String DIRECTORY = System.getProperty("user.home") + "/Documents/Uploads";

        List<String> fileNames = new ArrayList<>();
        for (MultipartFile file : files) {
            String fileName = file.getOriginalFilename();
            try {
                Path fileStorage = get(DIRECTORY, fileName).toAbsolutePath().normalize();
                copy(file.getInputStream(), fileStorage, REPLACE_EXISTING);
            } catch (Exception e) {
                exceptions.add(e.getMessage());
                e.printStackTrace();
            }
            fileNames.add(fileName);
        }

        if (!exceptions.isEmpty()) {
            return ResponseEntity.badRequest().body(exceptions);
        }

        return ResponseEntity.ok().body(fileNames);

    }

}

以下是Postman中的一个示例执行,它返回上传的文件列表:

如上图所示,后端代码工作正常。

上传.Component.ts

import { Component } from '@angular/core';
import { AppConfigService } from 'src/shared/services/app-config.service';
import getClassNameForExtension from 'font-awesome-filetypes';
import { HttpClient } from '@angular/common/http';
import { NgxSpinnerService } from 'ngx-spinner';

@Component({
  selector: 'my-app',
  templateUrl: './upload.component.html',
  styleUrls: ['./upload.component.scss'],
})
export class UploadComponent {
  files = [];
  totalSize: number = 0;
  maxUploadSize: number;
  fileExtensions: Array<string> = [];
  hasInvalidFile: boolean = false;

  constructor(
    private appConfigService: AppConfigService,
    private spinner: NgxSpinnerService,
    private http: HttpClient
  ) {}

  ngOnInit() {
    this.maxUploadSize = this.appConfigService.configData.maxUploadSize;
    if (this.appConfigService.configData.fileExtensions) {
      const extensions =
        this.appConfigService.configData.fileExtensions.split(',');
      extensions.forEach((ext) => {
        this.fileExtensions.push(ext.trim());
      });
    }
  }

  onFileDropped($event) {
    this.prepareFilesList($event);
  }

  fileBrowseHandler(files) {
    this.prepareFilesList(files);
  }

  deleteFile(index: number) {
    let newSize = 0;
    this.files.splice(index, 1);

    let allValid = true;
    this.files.forEach((file) => {
      if (file.invalidFileExtension) {
        allValid = false;
      }
      newSize += file.size;
    });

    this.hasInvalidFile = !allValid;
    this.totalSize = newSize;
  }

  uploadFilesSimulator(index: number) {
    setTimeout(() => {
      if (index === this.files.length) {
        return;
      } else {
        const progressInterval = setInterval(() => {
          if (this.files[index]) {
            if (this.files[index].progress === 100) {
              clearInterval(progressInterval);
              this.uploadFilesSimulator(index + 1);
            } else {
              this.files[index].progress += 5;
            }
          }
        }, 200);
      }
    }, 1000);
  }

  prepareFilesList(files: Array<any>) {
    for (const file of files) {
      // const ext = file.name.substr(file.name.lastIndexOf('.') + 1);
      file.progress = 0;

      const extension = file.name.split('.').pop();
      file.extension = extension;

      const className = getClassNameForExtension(extension);
      file.className = className;

      if (
        this.fileExtensions.length > 0 &&
        !this.fileExtensions.includes(extension)
      ) {
        file.invalidFileExtension = true;
        this.hasInvalidFile = true;
      }

      this.files.push(file);
      this.totalSize += file.size;
    }
    this.uploadFilesSimulator(0);
  }

  /**
   * Format size in bytes
   * @param bytes (File size in bytes)
   * @param decimals (Decimals point)
   */
  formatBytes(bytes) {
    if (bytes === 0) {
      return '0 Bytes';
    }
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  }

  upload() {
    const formData = new FormData();

    for (const file of this.files) {
      formData.append('files', file);
    }

    this.spinner.show();
    this.http.post(`http://localhost:16080/files/upload`, formData);
  }
}

上传组件.html

<div class="ui-g" style="height: 87vh">
  <!-- Spinner -->
  <ngx-spinner
    bdColor="rgba(189,188,188,0.6)"
    size="medium"
    color="#4a4848"
    type="ball-beat"
    [fullScreen]="true"
  >
  </ngx-spinner>
  <div
    class="ui-lg-12 ui-md-12 ui-g-12"
    style="padding: 1.25%; padding-bottom: 0; height: 99%"
  >
    <mat-card
      class="ui-lg-12 ui-md-12 ui-g-12 app-card"
      style="height: 100%; overflow: auto"
    >
      <!-- Card-header -->
      <div class="app-card-header flex-align-center ui-lg-12 ui-md-12 ui-g-12">
        <!-- Breadcrumbs -->
        <div class="ui-lg-12 ui-md-12 ui-g-12 p-0 d-flex align-center">
          <i
            [matTooltip]="''"
            matTooltipClass="mat-tool-cust"
            class="fas fa-table-cells-large app-icon-head ocular-app-card-header-icon"
          ></i>
          <span style="padding: 0 8px"> Data Processing </span>
        </div>
      </div>
      <!-- Card Content -->
      <div class="upload-container" (fileDropped)="onFileDropped($event)">
        <form method="post" enctype="multipart/form-data">
          <input
            type="file"
            #fileDropRef
            id="fileDropRef"
            multiple
            (change)="fileBrowseHandler($event.target.files)"
          />
          <i class="fas fa-regular fa-upload"></i>
          <h3>Drag and drop files here</h3>
          <h3>or</h3>
          <label
            for="fileDropRef"
            class="app-btn1 mat-raised-button mat-button-base"
            >Click here to browse files</label
          >
        </form>
      </div>
      <div class="files-list">
        <div class="single-file" *ngFor="let file of files; let i = index">
          <div class="file-icon">
            <i class="fa {{ file.className }}" style="font-size: 20px"></i>
          </div>
          <div class="info">
            <h4
              class="name"
              [ngClass]="file.invalidFileExtension ? 'strike-through' : ''"
            >
              {{ file?.name }}
            </h4>
            <p class="size">
              {{ formatBytes(file?.size) }}
            </p>
            <app-progress [progress]="file?.progress"></app-progress>
          </div>

          <div class="delete-ctn" (click)="deleteFile(i)">
            <i class="fas fa-trash action-icons" matTooltip="Delete"></i>
          </div>
        </div>
      </div>
      <div
        class="upload-btn-container"
        *ngIf="
          files.length > 0 && (hasInvalidFile || totalSize > maxUploadSize)
        "
      >
        <div style="justify-content: center; color: red">
          Files cannot be uploaded. Some files may not be in the supported
          format ({{ fileExtensions }}) or the total allowable upload size may
          have exceeded {{ formatBytes(maxUploadSize) }}.
        </div>
      </div>
      <div
        class="upload-btn-container"
        *ngIf="
          files.length > 0 && !hasInvalidFile && totalSize <= maxUploadSize
        "
      >
        <button
          type="submit"
          (click)="upload()"
          mat-raised-button
          class="app-btn2 save-btn"
        >
          Upload Files
        </button>
      </div>
    </mat-card>
  </div>
</div>

有效载荷

我收到了一个错误

nested exception is org.springframework.web.multipart.MultipartException: Current request is not a multipart request] with root cause
org.springframework.web.multipart.MultipartException: Current request is not a multipart request

我可以看到与Postman的有效负载略有不同,因为它提供了完整的路径。但出于安全目的,这在Java脚本中是不可能实现的。

ljsrvy3e

ljsrvy3e1#

提供标题Content-Type:文件,它应该可以解决问题。

this.http.post(`http://localhost:16080/files/upload`, formData, 
      headers: {
        'Content-Type': 'file'
      }
);

const _headers = new HttpHeaders({
   'Content-Type': 'file'
});

this.http.post(`http://localhost:16080/files/upload`, formData, 
      headers: _headers
);
c3frrgcw

c3frrgcw2#

我的原始代码正在按预期运行。问题出在我们的项目中,有一个HttpInterceptor。如果没有传递,它会自动附加一个标头'Content-Type', 'application/json'
我在http调用中添加了这一点。

const headers = new HttpHeaders().set('isUpload', 'true');
this.http.post(`http://localhost:16080/files/upload`, formData, {headers});

在拦截器中,我添加了一个条件以保留先前的逻辑。

import { Observable } from 'rxjs';
import { Injectable } from '@angular/core';
import {
  HttpInterceptor,
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpParams,
} from '@angular/common/http';
import { MatDialogRef } from '@angular/material/dialog';
import { MessageComponent } from '../message/message.component';

@Injectable({
  providedIn: 'root',
})
export class RequestInterceptorService implements HttpInterceptor {
  constructor() {}

  etagValue = undefined;

  // error dialog ref var
  dialogRef!: MatDialogRef<MessageComponent>;

  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    request = request.clone({
      withCredentials: true,
      headers: request.headers.set('calling-entity', 'UI'),
    });

    if (!request.headers.has('Content-Type')) {
      if (!request.headers.has('isUpload')) {
         request = request.clone({
           headers: request.headers.set('Content-Type', 'application/json'),
         });
      }
    }

    if (
      request.params instanceof CustomHttpParams &&
      request.params.etagValue
    ) {
      request = request.clone({
        setHeaders: {
          'If-Match': request.params.etagValue,
        },
      });
    }

    return next.handle(request).pipe();
  }
}

export class CustomHttpParams extends HttpParams {
  constructor(public etagValue: any) {
    super();
  }
}

在注解掉设置Content-Type的部分之后,它就像预期的那样工作了。

相关问题