Expo & SpringBoot Multipart Upload Problem - spring-boot

I have been trying for hours now, to upload a file and a JSON using multipart file upload. I use Expo React Native as the client and SpringBoot as the server.
I already tried many different versions. After reading into this a lot, this is how it should work:
In my Expo app I have this:
const formData = new FormData();
formData.append(
'document',
new Blob([JSON.stringify(json)], {
type: 'application/json'
}));
formData.append('file', {
uri: url,
type: data.type,
name
});
const xhr = new XMLHttpRequest();
xhr.open('POST', API_URL);
xhr.setRequestHeader('Authorization', 'Bearer ' + jwt);
xhr.onload = () => {
const response = JSON.parse(xhr.response);
console.log(response);
// ... do something with the successful response
};
xhr.onerror = e => {
console.log(e, 'upload failed');
};
xhr.ontimeout = e => {
console.log(e, 'upload timeout');
};
xhr.send(formData);
In my SpringBoot Backend I have this:
#PostMapping(value = "/api/upload")
public ResponseEntity<Void> uploadDocument(
#RequestPart("document") DocumentDTO document,
#RequestPart("file") MultipartFile file) {
// ... my business logic
}
Now without the document it would work, but as soon as I add the document I get this error:
o.z.problem.spring.common.AdviceTraits : Bad Request: Required request part 'document' is not present
As a workaround I will upload files as base64 encoded strings for now ... But I really don't understand why this doesn't work, because it should.
Similar issue I think:
https://github.com/facebook/react-native/issues/30623
Any help would be greatly appreciated.

Try your backend with this approach. (May try removing Blob in FE, just leave it as
json string)
Create a class to wrap both document and file.
#Data
public class FormDataModel {
private MultipartFile file;
private DocumentDTO document;
public void setDocument(String document) {
ObjectMapper mapper = new ObjectMapper();
// this requires try-catch block in fact
this.document = mapper.readValue(document, DocumentDTO.class);
}
}
Use #ModelAttribute at Controller
#PostMapping(value = "/api/upload", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<Void> uploadDocument(#ModelAttribute FormDataModel wrapper) {
// ... my business logic
}

Related

How to transfer temporary PDF/CSV from Backend(Spring) to Frontend(Angular)?

What I try to achive:
I've got service method that generate PDF/CSV on my backend and I want to save that pdf by pressing a button on my frontend.
My first attempt was to create file and send whole PDF/CSV by controller.
#PostMapping(value = "/export")
public File exportReport(
#RequestParam(value = "format", defaultValue = "PDF") ExportFileFormat format,
#RequestBody ExportBody exportBody) {
if (format.equals(ExportFormat.CSV)) {
return reportService.csvExportSummaryCustomerReport(exportBody);
}
if (format.equals(ExportFormat.PDF)) {
return reportService.pdfExportSummaryCustomerReport(exportBody);
}
throw new InvalidWorkingTimeSyntaxException(String.format("Format:%s is invalid.", format));
}
But this solution gave me an errors with
Access to XMLHttpRequest at
'file:///C:/Users/UserFolder/AppData/Local/Temp/csv6677594787854925068.csv'
from origin 'http://localhost:4200' has been blocked by CORS policy:
Cross origin requests are only supported for protocol schemes: http,
data, chrome, chrome-extension, https.
Ofc I tried set new set response header with 'Access-Control-Allow-Origin' : '*', but it didn't helped out. Same with chrome.exe --allow-file-access-from-files --disable-web-security.
Thats why I decided to another approach which is transfer bytes[] and on angular side create PDF/CSV file.
#PostMapping(value = "/export")
public ResponseEntity<byte[]> exportReport(
#RequestParam(value = "format", defaultValue = "pdf") ExportFileFormat format,
#RequestBody ExportBody exportBody) {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.set("Access-Control-Allow-Origin", "*");
if (format.equals(ExportFileFormat.CSV)) {
responseHeaders.setContentType(MediaType.valueOf("text/csv"));
return new ResponseEntity<>(reportService.csvExportSummaryCustomerReport(exportBody),
responseHeaders,
HttpStatus.OK);
}
if (format.equals(ExportFileFormat.PDF)) {
responseHeaders.setContentType(MediaType.APPLICATION_PDF);
return new ResponseEntity<>(reportService.pdfExportSummaryCustomerReport(exportBody),
responseHeaders,
HttpStatus.OK);
}
throw new InvalidExportFileFormatException(String.format("Format:%s is invalid.", format));
}
Now I added headers and backend seems ok.
After that I created service in frontened side:
exportReport(exportBody: ExportBody, format: String): Observable<Object> {
const exportUrl = `${this.reportsUrl}/export?format=${format}`;
if (format == "PDF") {
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/pdf',
'Accept': 'application/pdf'
})
};
return this.http.post(exportUrl, exportBody, httpOptions);
}
if (format == "CSV") {
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'text/csv',
'Accept': 'text/csv'
})
};
return this.http.post(exportUrl, exportBody, httpOptions);
}
}
Right now I wanted to used it just to print result.
downloadPdf() {
this.hoursWorkedForCustomersService.exportReport(this.exportBody, "PDF").subscribe(
result => {
console.log(result);
//saveAs(result, 'new.csv'); <- in the future.
}
);
}
Obviously in future I would've like to download file as PDF/CSV with e.g.
saveAs(result, 'new.pdf');
I've got an error 406. Response is:
POST http://localhost:4200/export?format=PDF 406.
TypeError: Cannot read property 'message' of null
at SafeSubscriber.next.handle.do.err [as _error] (error.interceptor.ts:25)
at SafeSubscriber.__tryOrSetError (Subscriber.js:240)
at SafeSubscriber.error (Subscriber.js:195)
at Subscriber._error (Subscriber.js:125)
at Subscriber.error (Subscriber.js:99)
at DoSubscriber._error (tap.js:84)
at DoSubscriber.error (Subscriber.js:99)
at XMLHttpRequest.onLoad (http.js:1825)
at ZoneDelegate.webpackJsonp../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:421)
at Object.onInvokeTask (core.js:4006)
Any ideas what I'm doing wrong?
Try splitting your backend method into two:
#PostMapping(value = "/export", params={"format=PDF"}, produces=MediaType.APPLICATION_PDF_VALUE)
public ResponseEntity<byte[]> generatePdf(){..}
#PostMapping(value = "/export", params={"format=CSV"}, produces="text/csv")
public ResponseEntity<byte[]> generateCsv(){..}
And please fix the Content-Type of the request: if you're sending JSON in UTF-8 this should work:
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json;charset=UTF-8',
'Accept': 'application/pdf'
})
};
BTW: Don't handle CORS headers in your Controller like this:
responseHeaders.set("Access-Control-Allow-Origin", "*");
Consider using #CrossOrigin(origins = "http://localhost:{your frontend server port}") at class or method level on your controller or globally like this:
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("http://localhost:{your frontend server port}");
}
};
}
cheers

How to send from Angular 5 vía POST to Spring controller multiple RequestParams

I have this Spring Controller:
#RequestMapping(value = "/create")
#ResponseBody
public ResponseEntity<?> create(
#RequestParam String name,
#RequestParam Integer startYear,
#RequestParam Integer endYear,
#RequestParam(required=false) MultipartFile polygons,
#RequestParam(required=false) Long reference
)
Is it possible to send those parameters from Angular via POST request?
I am trying this:
public createExperiment(): Observable<any> {
const headers = new HttpHeaders({'Content-Type': 'application/json'});
return this.http.post(this.backUrl + 'puerto/create', {name:'name'}, {headers: headers})
.map((res: any) =>
res
);
}
But I already receive this error:
error
:
{timestamp: 1527665099011, status: 400, error: "Bad Request", exception: "org.springframework.web.bind.MissingServletRequestParameterException", message: "Required String parameter 'name' is not present", …}
Why I get this error? and if I fix that, can I then send the MultiPartFile too?
I know I can fix this changing the controller and receiving the params via ResquestBody, but I would like to send the parameters separetely.
#RequestParam is used when you have url params.
http.post(url, null, { headers: headers, params: new HttpParams().set('name', 'fer') })
This will send the name along with the url like http://abcd.com/a/b?name=fer
If you want to send it as a post body, then define a model in your spring code and use #RequestBody annotation in the spring contoller.
Look here for an example.
It worked using a formData and ('Content-Type', 'multipart/form-data');
public createExperimentService(formData: FormData): Observable<any> {
const headers = new HttpHeaders();
headers.append('Content-Type', 'multipart/form-data');
return this.http.post(this.backUrl + 'puerto/create', formData, {headers: headers})
.map((res: any) =>
res
);
}
formData: FormData = new FormData();
createExperiment() {
this.formData.append('name', this.nameExperiment);
this.formData.append('startYear', this.startYear);
this.formData.append('endYear', this.endYear);
this.formData.append('reference', this.reference);
this.createExperimentService(this.formData).subscribe(res => {
console.log(res);
});
}
//This method loads a file
fileChange(event) {
const fileList: FileList = event.target.files;
if (fileList.length > 0) {
const file: File = fileList[0];
this.formData.append('polygons', file, file.name);
}
}

Spring Boot multipart.support.MissingServletRequestPartException while sending formData via POST request in Angular

I tried everything, and I am constantly getting this error from Spring Boot
Resolved exception caused by Handler execution: org.springframework.web.multipart.support.MissingServletRequestPartException: Required request part 'file' is not present
This is my Angular code. NOTE: I am using Http (not HttpClient) for my POST request.
updateUserProfilePicViaHttp(userId: number, imageFile: any) {
let headers: Headers = new Headers()
headers.append('Content-Type', 'multipart/form-data;boundary=imageUpload');
let formData: FormData = new FormData()
formData.append('file', imageFile, imageFile.name)
return this.http.post(this.baseUrl + `user/${userId}/profile_pic`, formData, { headers: headers })
}
This is my Spring Boot code
#RequestMapping(value="/{userId}/profile_pic",
method = RequestMethod.POST,
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
#ResponseBody
public ResponseEntity<User> uploadProfilePic(#PathVariable("userId") Integer id, #RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes){
IUserDao userDao = (IUserDao) getDao();
User user = null;
try {
user = userDao.saveAndUpdateProfilePic(id, file.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
if( user != null)
return new ResponseEntity<>(user, HttpStatus.OK);
else
return new ResponseEntity<>(user, HttpStatus.NOT_FOUND);
}
I think that there is something wrong with my Angular code, because when I send an image via Postman everything works fine!
Postman image:
Thanks!
EDIT: For some reason I tried replacing #RequestParam("file") MultipartFile file with #RequestParam MultipartFile file in my function.
Still getting the same error
It seems that when Spring says
Required request part 'file' is not present
It associates name 'file' with reference name of MultiPartFile file in my Spring boot function, not with #RequestParam('file').
EDIT2: I listened to Ravat and modified my code a little bit.
How I got imageFile?
Explanation:
#ViewChild('fileInput') myFile: ElementRef reference from <input type="file" #fileInput>
This is what is imageFile in my Angular function.
imageFile = this.myFile.nativeElement.files[0]
But still, same error...
After 6 hours of googling and explaining to Duck Overflow what is the problem, I stumbled upon this.
And I just removed my header
headers.append('Content-Type', 'multipart/form-data;boundary=imageUpload');
Final code
updateUserProfilePicViaHttp(userId: number, imageFile: any) {
let formData: FormData = new FormData()
formData.append('file', imageFile, imageFile.name)
return this.http.post(this.baseUrl + `user/${userId}/profile_pic`, formData)
}
The Issue is in following code code
updateUserProfilePicViaHttp(userId: number, image: string) {...}
I don't know about angular, here you are sending string in an image variable
that definitely won't work because it needs to be a file.

Unsupported Media Type with Content-Type header?

I'm getting an inexplicable error when I try to post an element by using the API of my backend. The API returns an error with code 415, related to Media Type:
Failed to load resource: the server responded with a status of 415 ()
My backend returns me this error:
Resolved exception caused by Handler execution: org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'text/plain;charset=UTF-8' not supported
EDIT: Error with Guru solution:
Resolved exception caused by Handler execution: org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'text/plain' not supported
The annoying thing is that I have added this header to my request:
Content-Type: application/json
And the body is correctly parsed into JSON format.
I am using Angular 5, and my backend has been developed by using Spring.
Angular client code:
postProject(project: Project) {
const headers: HttpHeaders = new HttpHeaders();
headers.append('Content-Type', 'application/json');
return this.http.post('http://localhost:8443/projects', project.body(), {headers: headers})
.map(response => response);
}
where body method is:
body() {
const object = {
id: this.id,
name: this.name,
'num_execs': this.num_execs
};
return JSON.stringify(object);
}
Spring API code:
#RequestMapping(value = "", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> addLocation(#RequestBody Project project) {
return new ResponseEntity<>(esProjectService.save(project), HttpStatus.OK);
}
Where RequestMapping of the class is /projects:
#RestController
#RequestMapping("/projects")
public class ProjectResource {
#RequestMapping(value = "", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> addLocation(#RequestBody Project project) {
return new ResponseEntity<>(esProjectService.save(project), HttpStatus.OK);
}
... OTHER METHODS...
}
I've already gone through this, one way to solve it is to specify the type #RequestPart (value = "nameOfResource" and consumes = {"multipart/form-data"}
Do not forget to specify the name of Content in Angular.
I hope it helps.
Here is an example below:
RequestMapping(value = "/add", method = RequestMethod.POST, consumes = {"multipart/form-data"}, produces = "application/json")
public ResponseEntity<?> add(#RequestPart(value = "image", required = true) MultipartFile image,
#RequestPart(value = "team", required = true) #Valid Team team, BindingResult bResult) {
}
In angular 5 HttpHeaders is immutable. Therefore, you should use it like this
let headers = new HttpHeaders({
'Content-Type': 'application/json',
'X-XSRF-TOKEN': token
});
or
let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'application/json');
headers = headers.append('X-XSRF-TOKEN', token);
Set the headers in this way and it should resolve your issue. I have put the sample code just to explain how you should add multiple headers. Don't put 'X-XSRF-TOKEN' if you don't require it.
I think the mistake comes from the fact that you want to consume application / json while you send plain / text.
I explain, in your service you specify
#RequestMapping(value = "", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
while in your front code you send plain / text
return JSON.stringify(object);
Just delete the JSON.stringify call and return the json object and all is well.
Change
body() {
const object = {
id: this.id,
name: this.name,
'num_execs': this.num_execs
};
return JSON.stringify(object);
}
to
body() {
const object = {
id: this.id,
name: this.name,
'num_execs': this.num_execs
};
return object;
}

Required request part 'file' is not present - Angular2 Post request

I am trying to get my file upload functionality done using Angular2 and SpringBoot. I can certify that my java code for the file uploading working fine since I have tested it successfully using Postman.
However, when it comes to sending the file from Angular2 front end, I am getting the HTTP 400 response saying Required request part 'file' is not present.
This is how I send the POST request from Angular2.
savePhoto(photoToSave: File) {
let formData: FormData = new FormData();
formData.append('file', photoToSave);
// this will be used to add headers to the requests conditionally later using Custom Request Options
this._globals.setRequestFrom("save-photo");
let savedPath = this._http
.post(this._endpointUrl + "save-photo", formData)
.map(
res => {
return res.json();
}
)
.catch(handleError);
return savedPath;
}
Note that I have written a CustomRequestOptions class which extends BaseRequestOptions in order to append Authorization header and Content Type header. Content Type header will be added conditionally.
Following is the code for that.
#Injectable()
export class CustomRequestOptions extends BaseRequestOptions {
constructor(private _globals: Globals) {
super();
this.headers.set('X-Requested-By', 'Angular 2');
this.headers.append('virglk', "vigamage");
}
merge(options?: RequestOptionsArgs): RequestOptions {
var newOptions = super.merge(options);
let hdr = this._globals.getAuthorization();
newOptions.headers.set("Authorization", hdr);
if(this._globals.getRequestFrom() != "save-photo"){
newOptions.headers.set('Content-Type', 'application/json');
}else{
//request coming from save photo
console.log("request coming from save photo");
}
return newOptions;
}
}
This conditional header appending is working fine. The purpose of doing that is if I add 'Content-Type', 'application/json' header to every request, file upload method in Spring controller will not accept it. (Returns http 415)
Everything seems to be fine. But I get Required request part 'file' is not present error response. Why is that? I am adding that param to the form Data.
let formData: FormData = new FormData();
formData.append('file', photoToSave);
This is the Spring Controller method for your reference.
#RequestMapping(method = RequestMethod.POST, value = "/tender/save-new/save-photo", consumes = {"multipart/form-data"})
public ResponseEntity<?> uploadPhoto(#RequestParam("file") MultipartFile file){
if (file.isEmpty()) {
ErrorResponse errorResponse = new ErrorResponse();
errorResponse.setMessage("DEBUG: Attached file is empty");
return new ResponseEntity<ErrorResponse>(errorResponse, HttpStatus.NOT_FOUND);
}
String returnPath = null;
try {
// upload stuff
} catch (IOException e) {
ErrorResponse errorResponse = new ErrorResponse();
errorResponse.setMessage(e.getMessage());
return new ResponseEntity<ErrorResponse> (errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
return new ResponseEntity<String>(returnPath, HttpStatus.OK);
}
EDIT - Adding the payload of the request captured by the browser
As you can see, the param "file" is available there.
Try to add
headers: {
'Content-Type': 'multipart/form-data'
},
to your
.post(this._endpointUrl + "save-photo", formData)
Change formData.append('file', photoToSave);
to              formData.append('file', this.photoToSave, this.photoToSave.name); and also add headers specifying the type of data you are passing to API, in your case it will be 'Content-Type': 'multipart/form-data'. Post the output here if it fails even after changing this.
Is there a chance that you're using zuul in a secondary app that is forwarding the request? I saw this with an update where the headers were stripped while forwarding a multi-part upload. I have a gatekeeper app which forwards requests using zuul to the actual service via a looking from eureka. I fixed it by modifying the url like this:
http://myserver.com/service/upload
to
http://myserver.com/zuul/service/upload
Suddenly the 'file' part of the upload header was no longer stripped away and discarded.
The cause, I suspect was a re-try mechanism which cached requests. On failure, it would re-submit the requests, but somehow for file uploads, it wasn't working properly.
To upload a file to the server, send your file inside a FormData and set content type as multipart/form-data.
export const uploadFile = (url, file, onUploadProgress) => {
let formData = new FormData();
formData.append("file", file);
return axios.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data;charset=UTF-8',
// other headers
},
onUploadProgress,
})
};
To handle file object, be careful with consumes attribute and #RequestPart annotation here.
#PostMapping(value = "/your-upload-path", consumes = "multipart/form-data")
public ResponseEntity<Object> uploadFile(#RequestPart("file") #Valid #NotNull #NotBlank MultipartFile file) {
// .. your service call or logic here
}

Resources