Search code examples
c#restmultipartflurl

Multipart POST to the Joplin REST API using C# and Flurl


I am currently working on a console app to import data into Joplin for Windows 10, using C# and Flurl. Joplin's API description can be found here.

I am trying to create a new resource in Joplin for a file on my system, so it can be attached to a Joplin note.

With CURL I can create the resource using command:

curl -F  "data=@c:\\temp\\Test.pptx" -F  "props={\"title\":\"my resource title\"}" http://localhost:41184/resources?token=MyToken

(note: it only works with "data=@c:\temp\Test.pptx", NOT with "data=c:\temp\Test.pptx")

When I try this with Flurl in c# I get a 400 response from Joplin, in the log I find:

Error: Resource cannot be created without a file at Api.action_resources (C:\Program Files\Joplin\resources\app.asar\lib\services\rest\Api.js:351:37) at Api.route (C:\Program Files\Joplin\resources\app.asar\lib\services\rest\Api.js:140:42) at execRequest (C:\Program Files\Joplin\resources\app.asar\lib\ClipperServer.js:157:39) at C:\Program Files\Joplin\resources\app.asar\lib\ClipperServer.js:185:8 at C:\Program Files\Joplin\resources\app.asar\node_modules\multiparty\index.js:136:9 at C:\Program Files\Joplin\resources\app.asar\node_modules\multiparty\index.js:115:9 at processTicksAndRejections (internal/process/task_queues.js:75:11)"

I have tried this so far:

        try
        {
            var url = BaseUrl
                .WithHeader("User_Agent", browserUserAgent)
                .AppendPathSegment("resources")
                .SetQueryParam("token", Token);

            using (var fs = new FileStream("c:\\temp\\Test.pptx", FileMode.Open, FileAccess.Read))
            {
                var resource = url.PostMultipartAsync(mp => mp
                        .AddJson("props", new { title = "test title" })
                        .AddFile("data", fs, "Test.pptx", "application/octet-stream")
                        )
                    .ReceiveJson<JoplinResource>()
                    .Result;
            }
        }

and:

        try
        {
            var url = BaseUrl
                .WithHeader("User_Agent", browserUserAgent)
                .AppendPathSegment("resources")
                .SetQueryParam("token", Token);

            var resource = url.PostMultipartAsync(mp => mp
                    .AddJson("props", new { title = "test title" })
                    .AddFile("data", "c:\\temp\\Test.pptx")
                    )
                .ReceiveJson<JoplinResource>()
                .Result;
        }

I hooked up fiddler to see what is the difference between my application and CURL.

Curl:

POST http://127.0.0.1:41184/resources?token=MyToken HTTP/1.1
Host: 127.0.0.1:41184
User-Agent: curl/7.70.0
Accept: */*
Connection: Keep-Alive
Content-Length: 33648
Content-Type: multipart/form-data; boundary=------------------------91ab181cbb0247ba

--------------------------91ab181cbb0247ba
Content-Disposition: form-data; name="props"

{"title":"my resource title"}
--------------------------91ab181cbb0247ba
Content-Disposition: form-data; name="data"; filename="Test.pptx"
Content-Type: application/octet-stream
...

My Console app:

POST http://localhost:41184/resources?token=MyToken HTTP/1.1
User_Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.122 Safari/537.36
Content-Type: multipart/form-data; boundary="f603841b-5c32-4e77-985a-69c2ffb6eed0"
Host: localhost:41184
Content-Length: 33612
Expect: 100-continue
Accept-Encoding: gzip, deflate

--f603841b-5c32-4e77-985a-69c2ffb6eed0
Content-Disposition: form-data; name=props

{"title":"My Resource"}
--f603841b-5c32-4e77-985a-69c2ffb6eed0
Content-Disposition: form-data; name=data; filename=Test.pptx; filename*=utf-8''Test.pptx
...

NOTE the differences:

  1. props and data are in quotes when using CURL, not with FLURL
  2. FLURL sends a second file name: filename*=utf-8''Test.pptx

How do I get this to work properly?


Solution

  • The issue was in the missing quotes for the "data" and "props":

        try
        {
            var url = BaseUrl
                .WithHeader("User_Agent", browserUserAgent)
                .AppendPathSegment("resources")
                .SetQueryParam("token", Token);
    
                var resource = url.PostMultipartAsync(mp => mp
                        .AddJson("\"props\"", new { title = "My Resource" })
                        .AddFile("\"data\"", "c:\\temp\\Test.pptx")
                        )
                    .ReceiveJson<JoplinResource>()
                    .Result;
        }
    

    Raw request header is now:

    POST http://localhost:41184/resources?token=MyToken HTTP/1.1
    User_Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.122 Safari/537.36
    Content-Type: multipart/form-data; boundary="c6b2377a-1240-4ae3-872f-fa24b643d3e0"
    Host: localhost:41184
    Content-Length: 33616
    Expect: 100-continue
    Accept-Encoding: gzip, deflate
    
    --c6b2377a-1240-4ae3-872f-fa24b643d3e0
    Content-Disposition: form-data; name="props"
    
    {"title":"My Resource"}
    --c6b2377a-1240-4ae3-872f-fa24b643d3e0
    Content-Disposition: form-data; name="data"; filename=Test.pptx; filename*=utf-8''Test.pptx
    ...
    

    And the Joplin REST service creates a new resource...