Hi everyone,
I've run into an issue with uploading binary attachments to the Business One Service Layer through HANA XS.
The business case goes as follows. The customer wants us to build an API in HANA XS. The API sits between a third party application and the Business One Service Layer. One of the endpoints should make it possible to create service calls, and add attachments to these service calls. The attachments are sent as BASE64 strings in the following format:
{ "businessPartnerCode": "C12345", "comments": "Please look into this", ... "attachments" : [ { "filename": "document.pdf", "mimeType": "application/pdf", "contents": "iVBORw0KGgoAAAANSUhEUgAAAMMAAADD…" } ] }
Creating the service call works without an issue.
After creating the service call I want to create the attachments, and then later on link them to the service call.
In the documentation of the Service Layer ("Working with SAP Business One Service Layer") on page 68 it details how attachments can be uploaded by sending them in a multipart request: "Attach source file from Windows".
When I perform a POST to the Service Layer endpoint for this (/b1s/v1/Attachments2) with Postman, I do the following:
This works as expected:
{ "odata.metadata": "https://***:50000/b1s/v1/$metadata#Attachments2/@Element", "AbsoluteEntry": "11533", "Attachments2_Lines": [ { "SourcePath": "/tmp/sap_b1_b1service0/ServiceLayer/Attachments2/", "FileName": "sap", "FileExtension": "png", "AttachmentDate": "2017-12-05", "UserID": "1", "Override": "tNO", "U_CorCategory": null } ] }
The file is created, and the entry is written to the ATC1 table of Business One.
However, when I try to perform the same thing in HANA XS, I keep getting an Internal Server Error, or just no result at all.
This is the body of my method which executes the request. I added comments to specify what each other method call does, and what the end result is:
{ var client = new $.net.http.Client(); // The client is instantiated correctly, and works for other calls var request = new $.web.WebRequest(this.determineHttpMethod(httpVerb), querystring); // - The request is instantiated correctly, and works for other calls // - The http method is set to $.net.http.POST // - querystring contains the path: /b1s/v1/Attachments2 this.setRequestHeaders(httpVerb, request, headers); // - Ability to add additional headers to the request // - None are specified this.setRequestBody(requestBody, true, request); // requestBody is undefined, and thus will not bet set this.setRequestCookies(request); // - This method will add the B1SESSION and ROUTEID cookies to the requests, which were received from the login call earlier in the class // - Login and logout works perfectly, and other calls to the Service Layer also work correctly this.addAttachments(attachments, request); // Detailed below client.request(request, this.readDestination()); // The destination is correctly parsed var response = client.getResponse(); // response is an empty object for the attachments call this.storeResponseCookies(response); // Store the B1SESSION and ROUTEID cookies for successive calls return this.formatResponse(querystring, response); // Format the response, and send it back }
The addAttachments method looks like this:
{ if (attachments == null || !Array.isArray(attachments)) { return; } this.setMultipart(request); // Override the Content-Type header of the request itself by setting it to multipart/form-data for (var attachmentKey in attachments) { var attachment = attachments[attachmentKey]; var entity = request.entities.create(); var cid = moment().toDate().getTime().toString(); // - Get the milliseconds since the Unix epoch with Moment.js // - Works as expected var contents = $.util.codec.decodeBase64(attachment.contents); // - Decode the base64 string into an ArrayBuffer // - I decoded the base64 string also on my machine, and it’s a correct representation of the binary file // - I checked the byteLength property to make sure the ArrayBuffer is created correctly request.parameters.set("content['" + attachment.filename + "']", cid); // Link the content and CID entity.headers.set('Content-Disposition', 'form-data; name="files[\'' + attachment.filename + '\']"; filename="' + attachment.filename + '"'); // - Set the Content-Disposition header // - The header is set correctly entity.headers.set('Content-Type', attachment.contentType); // The mime-type of the attachment, which is passed correctly entity.setBody(contents); // Set the ArrayBuffer as the body of the entity } }
As you can see, there is nothing overly complex going on here, but I don’t know why it works with Postman, but not with XS.
The addAttachments method runs correctly. I traced the output of every single line. The request itself is sent, but it seems it’s somehow refused by the Service Layer. I played with different mimetypes, one attachment, multiple attachments, additional headers,…
I suspect it has something to do with the boundaries, which are set automatically with Postman. But I can't look into the raw request, so I'm unable to verify this.