Send a file with multipart/form-data to OctoPrint REST API

#1

Hello there,
I'm having a hard time configuring the nodes correctly in order to send an uploaded file to OctoPrint REST. Here is my node setup however i't won't work unless you register your app in your local octoprint and get an API key to be set in the attached flow. (ops I'm new and I can't)

[{"id":"d601341.b493c48","type":"change","z":"9b9a811f.0afc7","name":"set payload","rules":[{"t":"set","p":"filename","pt":"msg","to":"req.files['file'][0].path","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":330,"y":2780,"wires":[["9f9cdcaf.bba298","131edc7f.8c3e4c","665a2eec.c1dd7"]]},{"id":"9f9cdcaf.bba298","type":"change","z":"9b9a811f.0afc7","name":"API-key","rules":[{"t":"set","p":"headers","pt":"msg","to":"{\"X-Api-Key\":\"xxxSetYourKey\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":500,"y":2780,"wires":[["6f15e914.283a4"]]},{"id":"2701ee04.a0bf52","type":"httpInMultipart","z":"9b9a811f.0afc7","name":"","url":"/octopi-upload","method":"post","fields":"[{\"name\": \"file\", \"maxCount\": 1}, {\"filename\":\"\"}]","swaggerDoc":"","x":130,"y":2780,"wires":[["d601341.b493c48"]]},{"id":"131edc7f.8c3e4c","type":"debug","z":"9b9a811f.0afc7","name":"","active":true,"tosidebar":true,"console":false,"complete":"req.files","x":550,"y":2700,"wires":[]},{"id":"665a2eec.c1dd7","type":"file in","z":"9b9a811f.0afc7","name":"","filename":"","format":"","chunk":false,"sendError":false,"x":330,"y":2900,"wires":[["377c8d6c.890e42"]]},{"id":"6f15e914.283a4","type":"function","z":"9b9a811f.0afc7","name":"Set Headers","func":"var file = msg.req.files['file'][0];\n\nmsg.headers = {};\n//msg.headers['Accept'] = 'application/json';\nmsg.headers['Content-Type']= 'multipart/form-data';\nmsg.headers['Content-Disposition']= `form-data; name=\"file\"; filename=${file.originalname}`\n\nmsg.form={}\nmsg.payload[\"file\"]=\"data:application/octet-stream;base64,${msg.payload.toString('base64')}\";\n//msg.payload;\n// \n\n\nreturn msg;","outputs":1,"noerr":0,"x":590,"y":2840,"wires":[["1e8ff8d3.b58997","f601f775.0cfab8"]]},{"id":"377c8d6c.890e42","type":"base64","z":"9b9a811f.0afc7","name":"","action":"","property":"payload","x":480,"y":2880,"wires":[["6f15e914.283a4"]]},{"id":"1e8ff8d3.b58997","type":"http request","z":"9b9a811f.0afc7","name":"to OctoPi POST /api/files/(string: location) ","method":"POST","ret":"txt","url":"192.168.1.5/api/files/local","tls":"","x":860,"y":2840,"wires":[["920a3e9d.04e958","495b41ca.5d1878"]]},{"id":"f601f775.0cfab8","type":"debug","z":"9b9a811f.0afc7","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":870,"y":2720,"wires":[]},{"id":"920a3e9d.04e958","type":"debug","z":"9b9a811f.0afc7","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":1070,"y":2780,"wires":[]},{"id":"495b41ca.5d1878","type":"http response","z":"9b9a811f.0afc7","name":"response","statusCode":"200","headers":{},"x":1090,"y":2920,"wires":[]},{"id":"c25041d2.d8bd88","type":"ui_template","z":"9b9a811f.0afc7","group":"e97f66fe.f451c","name":"upload Template","order":2,"width":0,"height":0,"format":"\n    <form id=\"fileUpload\" action=\"/octopi-upload\" method=\"post\" enctype=\"multipart/form-data\">\n    <input type=\"file\" name=\"file\" id=\"fileToUpload\">\n    <input type=\"submit\" value=\"upload gcode\" name=\"submit\" \n        style=\"background-color: chartreuse;\"\n        class=\"form-button\">\n    </form>\n    ","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":330,"y":2680,"wires":[[]]},{"id":"e97f66fe.f451c","type":"ui_group","z":"","name":"Files","tab":"81b4005b.665ae8","order":5,"disp":true,"width":"6","collapse":true},{"id":"81b4005b.665ae8","type":"ui_tab","z":"9b9a811f.0afc7","name":"3DPrint","icon":"camera_alt","order":2,"disabled":false,"hidden":false}]

I'll describe it briefly so you don't actually have to install octopi to help me out
In the flow an UI-template post a file using the httpInMultipart node endpoint. in msg.req.files there are the pointer to the temporary file like /tmp/1f9fbd1f8967f1c6cccbf2bbb483f45a the get correctly created.
This path is available under req.files['file'][0].path and since the File-in node it requires msg.filename I've set a change node for that.
The the common API-key header get set (you need yours) and the required headers for the OctoPi REST API for file upload get set in the Set Headers function node before being sent to the http request node.

I think OctoPi doesn't like they way I'm sending it as data:application/octet-stream;base64 in msg.payload["file"] I'm sending the payload of the File_in node with the correct real path!!!

Unfortunately the 400 Bad request error as a response from octopi is not really helpful and I don't know where to go from here :scream:

I've easily built an entire panel controlling and monitoring Octopy in node-red only the file upload is not working... can you help me out?
dashboard screenshot

#2

You can't just split and join the flow like that as the two messages will arrive at the function at different times... you can just add the api key in the other line after the base64 as you are just adding msg.headers and leaving the rest of the msg alone.

(However your API key is in that node - so you may wish to edit the flow you attached above to obscure/delete it)

#3

Hi dceejay and thanks for looking into it,
I've fixed it (API key headers should be set as a flow variable probably), unfortunately this is not the problem.
I've tried with and without base64, since should be already encoded in msg.payload["file"]="data:application/octet-stream;base64,${msg.payload.toString('base64')}";

don't know what to try out

#4

What version of OctoPi are you using?
What version of NR and node.js?

I'm also running OctoPi on a Pi with NR and I'll try to take a look later today (have an almost 2yr old coming over in a half hour and I'm her favorite toy :grin:)

#5

Thanks zenofmud to take a look I'm really running out off ideas and I'm so close because I found out that in the node-red log it actually says a little more:

 - [info] [base64:377c8d6c.890e42] Not a Base64 string - maybe we should encode it...

so it's clearly not recognizing that the file attached it's indeed encoded.
If I remove the base64 node it doesn't complain anymore and no error at all in the log but I still get that 400 Bad request

 [info] Node-RED version: v0.19.5
 [info] Node.js &#160;version: v10.15.0
 [info] Linux 4.14.79-v7+ arm LE
 [info] Loading palette nodes
 [info] Dashboard version 2.13.2 started at /ui
~/oprint/bin/octoprint --version
octoprint, version 1.3.10

The flow (again set your key)

[{"id":"131edc7f.8c3e4c","type":"debug","z":"9b9a811f.0afc7","name":"","active":true,"tosidebar":true,"console":false,"complete":"req.files","x":550,"y":2700,"wires":[]},{"id":"67ee6af2.24d00c","type":"ui_template","z":"9b9a811f.0afc7","group":"e97f66fe.f451c","name":"upload Template","order":2,"width":0,"height":0,"format":"\n    <form id=\"fileUpload\" action=\"/octopi-upload\" method=\"post\" enctype=\"multipart/form-data\">\n    <input type=\"file\" name=\"file\" id=\"fileToUpload\">\n    <input type=\"submit\" value=\"upload gcode\" name=\"submit\" \n        style=\"background-color: chartreuse;\"\n        class=\"form-button\">\n    </form>\n    ","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":130,"y":2700,"wires":[[]]},{"id":"2701ee04.a0bf52","type":"httpInMultipart","z":"9b9a811f.0afc7","name":"","url":"/octopi-upload","method":"post","fields":"[{\"name\": \"file\", \"maxCount\": 1}, {\"filename\":\"\"}]","swaggerDoc":"","x":130,"y":2780,"wires":[["d601341.b493c48"]]},{"id":"920a3e9d.04e958","type":"debug","z":"9b9a811f.0afc7","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":1070,"y":2780,"wires":[]},{"id":"d601341.b493c48","type":"change","z":"9b9a811f.0afc7","name":"set payload","rules":[{"t":"set","p":"filename","pt":"msg","to":"req.files['file'][0].path","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":330,"y":2780,"wires":[["131edc7f.8c3e4c","665a2eec.c1dd7"]]},{"id":"1e8ff8d3.b58997","type":"http request","z":"9b9a811f.0afc7","name":"to OctoPi POST /api/files/(string: location) ","method":"POST","ret":"txt","url":"192.168.1.5/api/files/local","tls":"","x":1000,"y":2840,"wires":[["920a3e9d.04e958","495b41ca.5d1878"]]},{"id":"9f9cdcaf.bba298","type":"change","z":"9b9a811f.0afc7","name":"API-key","rules":[{"t":"set","p":"headers","pt":"msg","to":"{\"X-Api-Key\":\"SET-YOURS\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":620,"y":2840,"wires":[["6f15e914.283a4"]]},{"id":"f601f775.0cfab8","type":"debug","z":"9b9a811f.0afc7","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":870,"y":2720,"wires":[]},{"id":"6f15e914.283a4","type":"function","z":"9b9a811f.0afc7","name":"Set Headers","func":"var file = msg.req.files['file'][0];\n\n//msg.headers = {};\nmsg.headers['Accept'] = 'application/json';\n//msg.headers['Content-Type']= 'multipart/form-data';\nmsg.headers['content-type']= 'multipart/form-data';\n//msg.headers['content-type']= 'application/x-www-form-urlencoded';\n\nmsg.headers['Content-Disposition']= `form-data; name=\"file\"; filename=${file.originalname}`\n\nmsg.form={}\n\nmsg.payload[\"file\"]=\"data:application/octet-stream;base64,${msg.payload.toString('base64')}\";\n// \n\n\nreturn msg;","outputs":1,"noerr":0,"x":690,"y":2940,"wires":[["f601f775.0cfab8","1e8ff8d3.b58997"]]},{"id":"495b41ca.5d1878","type":"http response","z":"9b9a811f.0afc7","name":"response","statusCode":"200","headers":{},"x":1200,"y":2900,"wires":[]},{"id":"665a2eec.c1dd7","type":"file in","z":"9b9a811f.0afc7","name":"","filename":"","format":"utf8","chunk":false,"sendError":false,"x":310,"y":2840,"wires":[["9f9cdcaf.bba298"]]},{"id":"377c8d6c.890e42","type":"base64","z":"9b9a811f.0afc7","name":"","action":"","property":"payload","x":460,"y":2880,"wires":[[]]},{"id":"fa161a5c.6024f8","type":"comment","z":"9b9a811f.0afc7","name":"Upload a file in octopi via the REST API","info":"","x":190,"y":2640,"wires":[]},{"id":"e97f66fe.f451c","type":"ui_group","z":"","name":"Files","tab":"81b4005b.665ae8","order":5,"disp":true,"width":"6","collapse":true},{"id":"81b4005b.665ae8","type":"ui_tab","z":"9b9a811f.0afc7","name":"3DPrint","icon":"camera_alt","order":2,"disabled":false,"hidden":false}]

I can't any working example of posting a file as a 'multipart/form-data' :confused:

#6

I should also add that in the OctoPrint logs I get

2019-02-04 17:53:49,475 - tornado.general - WARNING - 400 POST /api/files/local (::ffff:192.168.1.5): No multipart boundary supplied
2019-02-04 17:53:49,479 - tornado.access - WARNING - 400 POST /api/files/local (::ffff:192.168.1.5) 4.62ms

Resending the flow setting the file node to correctly output as buffer object (doesn't change the result)

[{"id":"131edc7f.8c3e4c","type":"debug","z":"9b9a811f.0afc7","name":"","active":true,"tosidebar":true,"console":false,"complete":"req.files","x":550,"y":2700,"wires":[]},{"id":"67ee6af2.24d00c","type":"ui_template","z":"9b9a811f.0afc7","group":"e97f66fe.f451c","name":"upload Template","order":2,"width":0,"height":0,"format":"\n    <form id=\"fileUpload\" action=\"/octopi-upload\" method=\"post\" enctype=\"multipart/form-data\">\n    <input type=\"file\" name=\"file\" id=\"fileToUpload\">\n    <input type=\"submit\" value=\"upload gcode\" name=\"submit\" \n        style=\"background-color: chartreuse;\"\n        class=\"form-button\">\n    </form>\n    ","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":130,"y":2700,"wires":[[]]},{"id":"2701ee04.a0bf52","type":"httpInMultipart","z":"9b9a811f.0afc7","name":"","url":"/octopi-upload","method":"post","fields":"[{\"name\": \"file\", \"maxCount\": 1}, {\"filename\":\"\"}]","swaggerDoc":"","x":130,"y":2780,"wires":[["d601341.b493c48"]]},{"id":"920a3e9d.04e958","type":"debug","z":"9b9a811f.0afc7","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":1070,"y":2780,"wires":[]},{"id":"d601341.b493c48","type":"change","z":"9b9a811f.0afc7","name":"set payload","rules":[{"t":"set","p":"filename","pt":"msg","to":"req.files['file'][0].path","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":330,"y":2780,"wires":[["131edc7f.8c3e4c","665a2eec.c1dd7"]]},{"id":"1e8ff8d3.b58997","type":"http request","z":"9b9a811f.0afc7","name":"to OctoPi POST /api/files/(string: location) ","method":"POST","ret":"txt","url":"192.168.1.5/api/files/local","tls":"","x":1000,"y":2840,"wires":[["920a3e9d.04e958","495b41ca.5d1878"]]},{"id":"9f9cdcaf.bba298","type":"change","z":"9b9a811f.0afc7","name":"API-key","rules":[{"t":"set","p":"headers","pt":"msg","to":"{\"X-Api-Key\":\"SET YOUR KEY\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":580,"y":2840,"wires":[["6f15e914.283a4"]]},{"id":"f601f775.0cfab8","type":"debug","z":"9b9a811f.0afc7","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":870,"y":2720,"wires":[]},{"id":"6f15e914.283a4","type":"function","z":"9b9a811f.0afc7","name":"Set Headers","func":"var file = msg.req.files['file'][0];\n\n//msg.headers = {};\nmsg.headers['Accept'] = 'application/json';\n//msg.headers['Content-Type']= 'multipart/form-data';\nmsg.headers['content-type']= 'multipart/form-data';\n//msg.headers['content-type']= 'application/x-www-form-urlencoded';\n\nmsg.headers['Content-Disposition']= `form-data; name=\"file\"; filename=${file.originalname}`\n\nvar _file = msg.payload;\nmsg.payload={};\nmsg.payload[\"file\"]=\"data:application/octet-stream;base64,${_file.toString('base64')}\";\n// \n\n\nreturn msg;","outputs":1,"noerr":0,"x":730,"y":2900,"wires":[["f601f775.0cfab8","1e8ff8d3.b58997"]]},{"id":"495b41ca.5d1878","type":"http response","z":"9b9a811f.0afc7","name":"response","statusCode":"200","headers":{},"x":1200,"y":2900,"wires":[]},{"id":"665a2eec.c1dd7","type":"file in","z":"9b9a811f.0afc7","name":"","filename":"","format":"","chunk":false,"sendError":false,"x":310,"y":2840,"wires":[["377c8d6c.890e42"]]},{"id":"377c8d6c.890e42","type":"base64","z":"9b9a811f.0afc7","name":"","action":"","property":"payload","x":400,"y":2920,"wires":[["9f9cdcaf.bba298"]]},{"id":"fa161a5c.6024f8","type":"comment","z":"9b9a811f.0afc7","name":"Upload a file in octopi via the REST API","info":"","x":190,"y":2640,"wires":[]},{"id":"e97f66fe.f451c","type":"ui_group","z":"","name":"Files","tab":"81b4005b.665ae8","order":5,"disp":true,"width":"6","collapse":true},{"id":"81b4005b.665ae8","type":"ui_tab","z":"9b9a811f.0afc7","name":"3DPrint","icon":"camera_alt","order":2,"disabled":false,"hidden":false}]
#7

In the REST docs it shows an example setting the boundary in many places...

POST /api/files/sdcard HTTP/1.1
Host: example.com
X-Api-Key: abcdef...
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryDeC2E3iWbTv1PwMC

------WebKitFormBoundaryDeC2E3iWbTv1PwMC
Content-Disposition: form-data; name="file"; filename="whistle_v2.gcode"
Content-Type: application/octet-stream

M109 T0 S220.000000
T0
G21
G90
...
------WebKitFormBoundaryDeC2E3iWbTv1PwMC
Content-Disposition: form-data; name="select"

true
------WebKitFormBoundaryDeC2E3iWbTv1PwMC
Content-Disposition: form-data; name="print"

true
------WebKitFormBoundaryDeC2E3iWbTv1PwMC--

How do I do it in node RED?
simply setting msg.headers['Content-Type'] = 'multipart/form-data ; boundary=----WebKitFormBoundaryDeC2E3iWbTv1PwMC';
make the boundary error disappear but the request hang in there until it times out.

maybe I need to specify it also in the message body somehow

#8

Finally having a chance to look at this but I have one question - why not just use OctoPrint?

I use OctoPrint to upload files for printing and I have a node-red instance on anotehr Pi that monitors it - even shows the vidoe from a webcam I've got attached to the pi running OctoPrint.

I use the mqtt plugin mainly to watch for the end of print and then it waits 10 miutes and if no other signals are received, it sends a shutdown to the Pi attached to the printer and a minute later sends a shutoff command to a smart plug that the pi and the printer are plugged into.

#9

I'm trying now with www-request-multipart node that probably is the right node for the job however I can't seems to get it working and there are no examples around :confused:

I'm willing to build a complete "control panel" for my printers, (also integrating a relay switch etc), being able to have one dashboard for many printers is very useful...
Moreover I plan to organize a Workshop and this can be a material for the course

I've all working for one printer (beside file upload)

1 Like
#10

which contrib multipart node are you using?

#11

that is node-red-contrib-http-request-multipart but no luck so far
The standard request node I have problem with setting boundary (see above)

#12

(@dceejay - please correct me if I am wrong)
I believe the issue is that the file-in node is trying to read the file from the machine running NR not the machine the browser is running on. i.e. if NR is running on a Pi and you are using a browser on a Windows machine, the file-in node will look for the file on the Pi.

You may need to use an ftp node to move the file from the Windows box to the Pi THEN snd it to OctoPrint.

#13

No zenofmud the file get correctly uploaded on the Pi see above (beside I don't use Windows nor mac machine since 15 years)
The real issue is that multipart form-data is not officially supported neither for http-in node nor for http-request.
I've discovered that is in the plan to officially support it since 2 years.

For http-in the contrib node-red-contrib-http-multipart does a perfect job and receive the file and it pass the name along in the flow.
For posting it to octopi I should use http-request but since is not supported I've tried with node-red-contrib-http-request-multipart but I give up because I'm not sure is working correctly, the node give me an error: "TypeError: source.on is not a function"

I'm now trying with node-red-contrib-send-multipart that throw "TypeError: Cannot read property 'statusCode' of undefined"

If anyone want to try out below is the flow to request the App token then the key from OctoPrint it get saved in a global.OctoPi_key and used in the subflow OctoPi API Key to set the headers for all the REST request.

[{"id":"1fa881f4.6e95d6","type":"subflow","name":"OctoPi API Key","info":"","category":"","in":[{"x":140,"y":120,"wires":[{"id":"de8fbdd1.b04078"}]}],"out":[{"x":460,"y":120,"wires":[{"id":"de8fbdd1.b04078","port":0}]}],"icon":"node-red/alert.png"},{"id":"de8fbdd1.b04078","type":"function","z":"1fa881f4.6e95d6","name":"get API key","func":"msg.headers = {};\nmsg.headers['X-Api-Key'] = global.get('OctoPi_key');\nreturn msg;\n","outputs":1,"noerr":0,"x":310,"y":120,"wires":[[]]},{"id":"fb3a7447.7eb7f8","type":"debug","z":"9b9a811f.0afc7","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":830,"y":1160,"wires":[]},{"id":"6521fca8.7e41dc","type":"http request","z":"9b9a811f.0afc7","name":"request token","method":"POST","ret":"txt","url":"192.168.1.5/plugin/appkeys/request","tls":"","x":360,"y":1020,"wires":[["36d5558.7d387aa"]]},{"id":"9e0eb1b9.a18038","type":"inject","z":"9b9a811f.0afc7","name":"request API token","topic":"","payload":"{\"app\":\"red-node\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":150,"y":1020,"wires":[["6521fca8.7e41dc"]]},{"id":"e250eb1f.7bbff8","type":"http request","z":"9b9a811f.0afc7","name":"return API key","method":"GET","ret":"txt","url":"","tls":"","x":540,"y":1100,"wires":[["fb3a7447.7eb7f8","fe246931.9c88b"]]},{"id":"3518b385.3fe54c","type":"inject","z":"9b9a811f.0afc7","name":"Poll Key","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":120,"y":1120,"wires":[[]]},{"id":"b3d8dbd4.544fc","type":"http request","z":"9b9a811f.0afc7","name":"Fetch list of existing application keys","method":"GET","ret":"txt","url":"192.168.1.5/api/plugin/appkeys","tls":"","x":530,"y":1220,"wires":[["fb3a7447.7eb7f8"]]},{"id":"70e549a5.1a2b38","type":"inject","z":"9b9a811f.0afc7","name":"List All","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":110,"y":1220,"wires":[["7b5891f1.cbe95"]]},{"id":"8298d6fc.da7bd","type":"comment","z":"9b9a811f.0afc7","name":"Generate API-KEY for OctoPrint","info":"","x":170,"y":940,"wires":[]},{"id":"c2be721c.dd9248","type":"comment","z":"9b9a811f.0afc7","name":"#1 request API token","info":"","x":140,"y":980,"wires":[]},{"id":"e67f38d3.df7bd","type":"comment","z":"9b9a811f.0afc7","name":"#2 Poll for decision on existing request","info":"","x":190,"y":1080,"wires":[]},{"id":"8afd6236.6d1a18","type":"function","z":"9b9a811f.0afc7","name":"set API key","func":"global.set('OctoPi_key', msg.payload.api_key)\nreturn msg;","outputs":1,"noerr":0,"x":910,"y":1100,"wires":[[]]},{"id":"36d5558.7d387aa","type":"json","z":"9b9a811f.0afc7","name":"","property":"payload","action":"","pretty":false,"x":570,"y":1020,"wires":[["1b521831.8d621"]]},{"id":"b261ee25.672178","type":"debug","z":"9b9a811f.0afc7","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":990,"y":940,"wires":[]},{"id":"1b521831.8d621","type":"function","z":"9b9a811f.0afc7","name":"set poll url","func":"msg.url = null\nmsg.url = \"192.168.1.5/plugin/appkeys/request/\"+msg.payload.app_token\nreturn msg;","outputs":1,"noerr":0,"x":730,"y":1020,"wires":[["b261ee25.672178","ccf34dd8.f8cd9"]]},{"id":"ccf34dd8.f8cd9","type":"delay","z":"9b9a811f.0afc7","name":"wait 5 sec confirm in OctoPrint UI","pauseType":"delay","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":1000,"y":1020,"wires":[["e250eb1f.7bbff8"]]},{"id":"fe246931.9c88b","type":"json","z":"9b9a811f.0afc7","name":"","property":"payload","action":"","pretty":false,"x":770,"y":1100,"wires":[["8afd6236.6d1a18"]]},{"id":"e6836b36.94781","type":"comment","z":"9b9a811f.0afc7","name":"#3 List All allowed apps","info":"Verify that the API key is working listing all the allowed apps","x":140,"y":1160,"wires":[]},{"id":"7b5891f1.cbe95","type":"subflow:1fa881f4.6e95d6","z":"9b9a811f.0afc7","name":"","x":260,"y":1220,"wires":[["b3d8dbd4.544fc","14086af5.0b7a45"]]},{"id":"14086af5.0b7a45","type":"debug","z":"9b9a811f.0afc7","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":830,"y":1240,"wires":[]},{"id":"d8c75790.7e53","type":"debug","z":"9b9a811f.0afc7","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":590,"y":1160,"wires":[]},{"id":"54bb9a91.90cc44","type":"inject","z":"9b9a811f.0afc7","name":"","topic":"","payload":"OctoPi_key","payloadType":"global","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":370,"y":1160,"wires":[["d8c75790.7e53"]]}]

And here is how I've tried to use node-red-contrib-send-multipart

[{"id":"1fa881f4.6e95d6","type":"subflow","name":"OctoPi API Key","info":"","category":"","in":[{"x":140,"y":120,"wires":[{"id":"de8fbdd1.b04078"}]}],"out":[{"x":460,"y":120,"wires":[{"id":"de8fbdd1.b04078","port":0}]}],"icon":"node-red/alert.png"},{"id":"de8fbdd1.b04078","type":"function","z":"1fa881f4.6e95d6","name":"get API key","func":"msg.headers = {};\nmsg.headers['X-Api-Key'] = global.get('OctoPi_key');\nreturn msg;\n","outputs":1,"noerr":0,"x":310,"y":120,"wires":[[]]},{"id":"2701ee04.a0bf52","type":"httpInMultipart","z":"9b9a811f.0afc7","name":"","url":"/octopi-upload","method":"post","fields":"[{\"name\": \"file\", \"maxCount\": 1}, {\"filename\":\"\"}]","swaggerDoc":"","x":130,"y":2780,"wires":[["d601341.b493c48"]]},{"id":"d601341.b493c48","type":"change","z":"9b9a811f.0afc7","name":"set payload","rules":[{"t":"set","p":"filename","pt":"msg","to":"req.files['file'][0].path","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":330,"y":2780,"wires":[["131edc7f.8c3e4c","665a2eec.c1dd7"]]},{"id":"131edc7f.8c3e4c","type":"debug","z":"9b9a811f.0afc7","name":"","active":true,"tosidebar":true,"console":false,"complete":"req.files","x":550,"y":2700,"wires":[]},{"id":"665a2eec.c1dd7","type":"file in","z":"9b9a811f.0afc7","name":"","filename":"","format":"","chunk":false,"sendError":false,"x":310,"y":2840,"wires":[["377c8d6c.890e42"]]},{"id":"377c8d6c.890e42","type":"base64","z":"9b9a811f.0afc7","name":"","action":"","property":"payload","x":340,"y":2900,"wires":[["c84a8565.424c8"]]},{"id":"c84a8565.424c8","type":"subflow:1fa881f4.6e95d6","z":"9b9a811f.0afc7","name":"","x":280,"y":3000,"wires":[["f75343ff.ea4bf"]]},{"id":"f75343ff.ea4bf","type":"function","z":"9b9a811f.0afc7","name":"Set Headers","func":"var file = msg.req.files['file'][0];\n\n//msg.headers = {};\n//msg.headers['Accept'] = 'application/json';\nmsg.headers['Content-Type'] = 'multipart/form-data';\n\nmsg.headers['Content-Disposition'] = `form-data; name=\"file\"; filename=\"${file.originalname}\";`\n\nmsg.filepath = file\n/*\nvar buffer = msg.payload;\nmsg.form={}\nmsg.payload={\n    \"file\" : [buffer]\n    };\n//\"${buffer.toString('base64')}\"\n//\"data:application/octet-stream;base64,${buffer.toString('base64')}\"\n*/\n\n\nreturn msg;","outputs":1,"noerr":0,"x":570,"y":2980,"wires":[["a2efc04a.6800e"]]},{"id":"a2efc04a.6800e","type":"http-send-multipart","z":"9b9a811f.0afc7","name":"","ret":"txt","filepath":"","url":"192.168.1.5/api/files/local","tls":"","x":790,"y":2980,"wires":[["153617db.4665a8"]]},{"id":"153617db.4665a8","type":"http response","z":"9b9a811f.0afc7","name":"response","statusCode":"200","headers":{},"x":1080,"y":2980,"wires":[]},{"id":"67ee6af2.24d00c","type":"ui_template","z":"9b9a811f.0afc7","group":"e97f66fe.f451c","name":"upload Template","order":2,"width":0,"height":0,"format":"\n    <form id=\"fileUpload\" action=\"/octopi-upload\" method=\"post\" enctype=\"multipart/form-data\">\n    <input type=\"file\" name=\"file\" id=\"fileToUpload\">\n    <input type=\"submit\" value=\"upload gcode\" name=\"submit\" \n        style=\"background-color: chartreuse;\"\n        class=\"form-button\">\n    </form>\n    ","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":130,"y":2700,"wires":[[]]},{"id":"fa161a5c.6024f8","type":"comment","z":"9b9a811f.0afc7","name":"Upload a file in octopi via the REST API","info":"","x":190,"y":2640,"wires":[]},{"id":"e97f66fe.f451c","type":"ui_group","z":"","name":"Files","tab":"81b4005b.665ae8","order":5,"disp":true,"width":"6","collapse":true},{"id":"81b4005b.665ae8","type":"ui_tab","z":"9b9a811f.0afc7","name":"3DPrint","icon":"camera_alt","order":2,"disabled":false,"hidden":false}]
#14

Two issues I see

  1. in httpSendMultipart it says:
Provides a node for posting multipart form-data.

The URL must be configured in the node.

url, if set, is used as the url of the request. Must start with http: or https:

yours doesn't
2) in your 'Set Headers' function you are setting

var file = msg.req.files['file'][0];
.
.
.
msg.filepath = file

I suggest you add a debug node (set to display complete msg object) to the output of the 'OctoPi API Key' node and see what you are getting for the file path

#15

That isn't 100% true. The HTTP In node does support file uploads now - select Post and tick the box to enable file uploads.

To send a multipart upload from the HTTP Request node is more work, but doable.

Here's a Function that will construct a suitable message to pass to the HTTP Request node:

msg.headers = {
    "Content-Type": "multipart/form-data; boundary=------------------------d74496d66958873e"
}


msg.payload = '--------------------------d74496d66958873e\r\n'+
'Content-Disposition: form-data; name="select"\r\n'+
'\r\n'+
'true\r\n'+
'--------------------------d74496d66958873e\r\n'+
'Content-Disposition: form-data; name="print"\r\n'+
'\r\n'+
'true\r\n'+
'--------------------------d74496d66958873e\r\n'+
'Content-Disposition: form-data; name="file"; filename="whistle_v2.gcode"\r\n'+
'Content-Type: application/octet-stream\r\n'+
'\r\n'+
'contents of the file\r\n'+
'--------------------------d74496d66958873e--\r\n';


return msg;

I've based the content from the example request you shared earlier.

A couple key points

  • you need to use \r\n for newlines
  • the boundary string set in the header defines how the separate form parts are separated in the body. When it appears in the body, it has two extra -- in front. The very last one also has -- added to the end. (Took quite a bit of googling and examing curl trace to spot that)

Hopefully from that you can insert the contents of the file you want to upload at the appropriate point.

#16

Thanks knolleary I didn't know I can just compose the payload this way!
But how do I put the contents of the file.
In my flow, as you can see, I've used the file-in node that send it as "Buffer object" then base64 node then got inserted as ${buffer.toString('base64')} is this correct?

#17

I've added a more complete example to the flow library here: https://flows.nodered.org/flow/cbf44e064b9406a1175a7e8d589f66ac

Let me know if that helps or if anything needs clarifying there.

#18

IT WORKED! I was indeed able to send it adapting your example! Goog job!

#19

How did you end up adding the contents of the file. I am getting "Error: write EPIPE" when I send this via POST, which AFAIK means that octoprint is dropping the connection before the information is uploaded because it is not in the format octoprint is expecting

`msg.headers = {
    "Content-Type": "multipart/form-data; boundary=------------------------d74496d66958873e",
    'Content-Length': "Buffer.byteLength(data)"
}

msg.payload = '--------------------------d74496d66958873e\r\n'+
'Content-Disposition: form-data; name="select"\r\n'+
'\r\n'+
'true\r\n'+
'--------------------------d74496d66958873e\r\n'+
'Content-Disposition: form-data; name="print"\r\n'+
'\r\n'+
'true\r\n'+
'--------------------------d74496d66958873e\r\n'+
'Content-Disposition: form-data; name="file"; filename="'+msg.filename+'"\r\n'+
'Content-Type: application/octet-stream\r\n'+
'\r\n'+
msg.payload+'\r\n'+
'--------------------------d74496d66958873e--\r\n';


return msg;`

and this is how the nodes are set up

[{"id":"3f7e7ffa.4734","type":"ui_template","z":"d0969e5.ee64a6","group":"bc476fb4.91eae","name":"Upload HTML","order":13,"width":"8","height":"2","format":"<h1>Upload</h1>\n\n<form action=\"/temp\" method=\"POST\" enctype=\"multipart/form-data\">\n    <input type=\"file\" name=\"myFile\" />\n    <input type=\"submit\" value=\"Submit\">\n</form>","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":1300,"y":280,"wires":[[]]},{"id":"1e25fc5f.8406f4","type":"change","z":"d0969e5.ee64a6","name":"AA File Upload","rules":[{"t":"set","p":"url","pt":"msg","to":"racka/aa/api/files/local?apikey=","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":2160,"y":280,"wires":[["396f0b7f.485e44"]]},{"id":"396f0b7f.485e44","type":"http request","z":"d0969e5.ee64a6","name":"","method":"POST","ret":"txt","paytoqs":false,"url":"","tls":"","proxy":"","authType":"basic","x":2350,"y":280,"wires":[[]]},{"id":"ae8c13a7.6bfe1","type":"function","z":"d0969e5.ee64a6","name":"Format the header and payload","func":"msg.headers = {\n    \"Content-Type\": \"multipart/form-data; boundary=------------------------d74496d66958873e\",\n    'Content-Length': \"Buffer.byteLength(data)\"\n}\n\n\nmsg.payload = '--------------------------d74496d66958873e\\r\\n'+\n'Content-Disposition: form-data; name=\"select\"\\r\\n'+\n'\\r\\n'+\n'true\\r\\n'+\n'--------------------------d74496d66958873e\\r\\n'+\n'Content-Disposition: form-data; name=\"print\"\\r\\n'+\n'\\r\\n'+\n'true\\r\\n'+\n'--------------------------d74496d66958873e\\r\\n'+\n'Content-Disposition: form-data; name=\"file\"; filename=\"'+msg.filename+'\"\\r\\n'+\n'Content-Type: application/octet-stream\\r\\n'+\n'\\r\\n'+\nmsg.payload+'\\r\\n'+\n'--------------------------d74496d66958873e--\\r\\n';\n\n\nreturn msg;","outputs":1,"noerr":0,"x":1810,"y":360,"wires":[["1e25fc5f.8406f4","b8ce260.d4f45d8"]]},{"id":"c000a6a1.93de68","type":"http in","z":"d0969e5.ee64a6","name":"","url":"/temp","method":"post","upload":true,"swaggerDoc":"","x":1430,"y":360,"wires":[["feb4aac.a06d158"]]},{"id":"b8ce260.d4f45d8","type":"debug","z":"d0969e5.ee64a6","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":2290,"y":400,"wires":[]},{"id":"feb4aac.a06d158","type":"function","z":"d0969e5.ee64a6","name":"Gcode","func":"var msg = {payload: msg.req.files[0].buffer.toString(), filename: msg.req.files[0].originalname}\nreturn msg;","outputs":1,"noerr":0,"x":1590,"y":360,"wires":[["ae8c13a7.6bfe1"]]},{"id":"bc476fb4.91eae","type":"ui_group","z":"","name":"AA","tab":"86ac15ae.8d8e88","order":1,"disp":true,"width":"10","collapse":true},{"id":"86ac15ae.8d8e88","type":"ui_tab","z":"","name":"Overview","icon":"dashboard","disabled":false,"hidden":false}]
#20

Open your gcode function,

  1. do you see any thing like a yellow triangle?

  2. What do you see when you hover over it?

  3. What do you think that means?

  4. have you read https://nodered.org/docs/user-guide/messages ?

  5. do you understand it?

  6. Put a debug node - set to display 'complete msg object' - on your http request what does it show?