3

I am not able to send my "multipart/form-data' file to this API. If I use POSTMAN it's working but with the https post method it seems that netsuite doesn't recognize the "form-data" content-type. Somebody knows how to send a form-data with SuiteScript 2 ? Here is a part of my code:

var fileObj = file.create({
                name: invoiceNumber + '_ubl.xml',
                fileType: file.Type.XMLDOC,
                contents: einvoicecontentwithpdf,
                folder : 120,
                isOnline : false
            });

var headers = {
                    'Authorization': 'Basic xxxxxxxxxxxxxxxxxxxxx',
                    'Content-Type':  'multipart/form-data'
            };

            var response = https.post({
                url: 'https://community-api-uat-1-2.nxt.uat.unifiedpost.com/api/universal_connector/v1/uc/ar',
                body: {
                    'file': fileObj                    
                },
                headers: headers 
            }); 
hesiode
  • 33
  • 1
  • 6

1 Answers1

11

You have to build the whole body yourself.

This works as much as I've tested so far. Code may look a little non-idomatic because it's compiled from Typescript.

Note that to include non-text files this would have to be adapted to base64 encode the file bodies.

/**
 * multiPartUpload.js
 * @NApiVersion 2.x
 */
define(["require", "exports", "N/https", "N/file", "N/log"], function (require, exports, http, file, log) {
    Object.defineProperty(exports, "__esModule", { value: true });
    var types = {};
    types[file.Type.AUTOCAD] = 'application/x-autocad';
    types[file.Type.BMPIMAGE] = 'image/x-xbitmap';
    types[file.Type.CSV] = 'text/csv';
    types[file.Type.EXCEL] = 'application/vnd.ms-excel';
    types[file.Type.FLASH] = 'application/x-shockwave-flash';
    types[file.Type.GIFIMAGE] = 'image/gif';
    types[file.Type.GZIP] = 'application/x-gzip-compressed';
    types[file.Type.HTMLDOC] = 'text/html';
    types[file.Type.ICON] = 'image/ico';
    types[file.Type.JAVASCRIPT] = 'text/javascript';
    types[file.Type.JPGIMAGE] = 'image/jpeg';
    types[file.Type.JSON] = 'application/json';
    types[file.Type.MESSAGERFC] = 'message/rfc822';
    types[file.Type.MP3] = 'audio/mpeg';
    types[file.Type.MPEGMOVIE] = 'video/mpeg';
    types[file.Type.MSPROJECT] = 'application/vnd.ms-project';
    types[file.Type.PDF] = 'application/pdf';
    types[file.Type.PJPGIMAGE] = 'image/pjpeg';
    types[file.Type.PLAINTEXT] = 'text/plain';
    types[file.Type.PNGIMAGE] = 'image/x-png';
    types[file.Type.POSTSCRIPT] = 'application/postscript';
    types[file.Type.POWERPOINT] = 'application/vnd.ms-powerpoint';
    types[file.Type.QUICKTIME] = 'video/quicktime';
    types[file.Type.RTF] = 'application/rtf';
    types[file.Type.SMS] = 'application/sms';
    types[file.Type.STYLESHEET] = 'text/css';
    types[file.Type.TIFFIMAGE] = 'image/tiff';
    types[file.Type.VISIO] = 'application/vnd.visio';
    types[file.Type.WORD] = 'application/msword';
    types[file.Type.XMLDOC] = 'text/xml';
    types[file.Type.ZIP] = 'application/zip';
    function getContentType(f) {
        var mime = types[f.fileType];
        var charset = f.encoding;
        var ct = 'Content-Type: ' + mime + (charset ? ';charset=' + charset : '');
        log.debug({ title: 'content for ' + f.name, details: ct });
        return ct;
    }
    function isFile(o) {
        return (typeof o == 'object' && typeof o.fileType != 'undefined');
    }
    function partIsBinary(contentType) {
        if (contentType.indexOf('text/') === 0)
            return false;
        switch (contentType) {
        case 'application/json':
        // check 'application/rtf' etc.
            return false;
        default: return true;
        }
    }
    /**
     * Creates a multipart upload
     * @param {string} url     to post to
     * @param {object} headers key/value of headers; include Auth headers if needed
     * @param {array} parts   array of {name:string, value:file|string}
     */
    function uploadParts(url, headers, parts) {
        var boundary = 'someuniqueboundaryasciistring';
        headers['content-type'] = 'multipart/form-data; boundary=' + boundary;
        // Body
        var body = [];
        parts.forEach(function (p, idx) {
            var partIsFile = isFile(p.value);
            body.push('--' + boundary);
            body.push('Content-Disposition: form-data; name="' + p.name + '"' + (partIsFile ? ('; filename="' + p.value.name + '"') : ''));
            if (partIsFile) { 
                var ct = getContentType(p.value);
                body.push(ct);
                if(partIsBinary(ct)){
                    body.push('Content-Transfer-Encoding: base64');
                }
            }
            body.push('');
            body.push(partIsFile ? p.value.getContents() : p.value);
            if (idx == parts.length - 1) {
                body.push('--' + boundary + '--');
                body.push('');
            }
        });
        // Submit Request
        try {
            var response = http.post({
                url: url,
                headers: headers,
                body: body.join('\r\n')
            });
            return response;
        }
        catch (e) {
            log.error({ title: 'Failed to submit file', details: (e.message || e.toString()) + (e.getStackTrace ? (' \n \n' + e.getStackTrace().join(' \n')) : '') });
        }
    }
    exports.uploadParts = uploadParts;
    ;
});

a simple test:

/**
 *@NApiVersion 2.x
 *@NScriptType Suitelet
 */
define(["require", "exports", "N/file", "./multiPartUpload"], function (require, exports, file, multiPartUpload_1) {
    Object.defineProperty(exports, "__esModule", { value: true });
    function onRequest(context) {
        var files = [
            { name: 'f1', value: file.load({ id: 1056 }) }, // file cabinet ids; you can use dynamic files
            { name:'t1', value:'test'},
            { name: 'f2', value: file.load({ id: 7479 }) }
        ];
        var resp = multiPartUpload_1.uploadParts('https://your-test-server/', {}, files);
        context.response.write({ output: resp.body });
    }
    exports.onRequest = onRequest;
});
bknights
  • 14,408
  • 2
  • 18
  • 31
  • Perfect. It's working, you're amazing :) But it's crazy that NetSuite doesn't allow us to do that natively – hesiode Oct 27 '17 at 10:01
  • well that is native :-) But yes these outbound http restrictions have been maddening for years. – bknights Oct 27 '17 at 18:21
  • 1
    Hi @bknights, why did you use: Object.defineProperty(exports, "__esModule", { value: true }); – user4076248 Dec 12 '17 at 12:09
  • @user4076248 It's compiled from Typescript. Typescript does that when tsconfig.json declares AMD modules. AMD is the convention that Netsuite supports without shimming. It seems to be benign. – bknights Dec 12 '17 at 15:51
  • @bknights thanks for explaining that, how did you set that up - is there a documentation or blog that I can refer to - I don't know Typescript but can we use ECMAScript similarly. – user4076248 Dec 12 '17 at 23:02
  • 1
    I@user4076248 you don’t really need any of that. If you are using ecmascript just use the SS2.0 script samples – bknights Dec 13 '17 at 03:09
  • This isn't possible with non-text files because the file.getContents() always returns a base64 string. NetSuite server-side does not have what is needed to support this, even when trying to use suitescript 2.1 – dcrs May 21 '20 at 19:40
  • Actually since SS2 and being able to reliably get the contents of binary files as base64 means you can now include those too by inserting `body.push('Content-Transfer-Encoding: base64');`for binary files – bknights May 21 '20 at 20:01
  • 2
    @bknights I tried your solution, but it gave an error saying partIsBinary is not defined. Is the method missing? – Markante Makrele Feb 03 '21 at 14:49
  • is there some special format to get JSON files to work? I keep getting 409 error – Adrian May 18 '23 at 15:40
  • There shouldn't be. Check whether Netsuite loads it as the original text or if its content is base64. Otherwise try to get detailed server logs on the receipt side. – bknights May 18 '23 at 17:07
  • @bknights is this approach is for `uploadType=multipart`. – HiddenOne1254 May 25 '23 at 06:20
  • @HiddenOne1254 Specifically this is a standard mime multipart. You can use it as a starting point for creating a RFC 2387 body as referenced in Google's Drive upload https://developers.google.com/drive/api/guides/manage-uploads#http_1 – bknights May 25 '23 at 16:28