Sync Error for selfhosted server

I have been working on creating a self hosted server using cloudflare tunnels as an access point into my newtwork. I have my setup everything via docker and I am about to access the server landing page and log in via my cloudflare tunnel but when I try to "Check synchronization configurations" I keep on getting this error:

Error. Please check that URL, username, password, etc. are correct and that the sync target is accessible. The reported error was:
Unexpected token < in JSON at position 0

I have tried on the mobile and desktop apps with the same results. I have tried to use local and LTE as an access point. I created a new user to see if that would work but it still wouldn't let me connect.

@Stencoss welcome to the forum.

Could it be that Joplin is expecting a reply in JSON format but is being sent some kind of HTML error page by cloudflare or the server; a file starting with an "unexpected" < of the tag <html> rather than the expected { of JSON?

Checking the Joplin logs might reveal a bit more information. How to enable debugging | Joplin

Hey dpoulton!

I enabled the development tools and I can see the error but to be honest I am not able to understand it any better.

C:\Users\me\AppDa…n\lib\Logger.js:190 09:03:31: JoplinServerApi: curl -v -X POST -H "X-API-MIN-VERSION: 2.6.0" -H "Content-Type: application/json" -H "Content-Length: 57" --data '{"email":"","password":"******"}' 'https://my.cloudflare.tunnel/api/sessions'
C:\Users\me\AppDa…n\lib\Logger.js:190 09:03:31: JoplinServerApi: Code: undefined
C:\Users\me\AppDa…n\lib\Logger.js:190 09:03:31: JoplinServerApi: SyntaxError: Unexpected token < in JSON at position 0
    at JSON.parse (<anonymous>)
    at JoplinServerApi.<anonymous> (C:\Users\me\AppDa…ServerApi.js:200:42)
    at (<anonymous>)
    at C:\Users\me\AppDa…inServerApi.js:8:71
    at new Promise (<anonymous>)
    at __awaiter (C:\Users\me\AppDa…inServerApi.js:4:12)
    at loadResponseJson (C:\Users\me\AppDa…ServerApi.js:195:48)
    at JoplinServerApi.<anonymous> (C:\Users\me\AppDa…ServerApi.js:236:38)
    at (<anonymous>)
    at fulfilled (C:\Users\me\AppDa…inServerApi.js:5:58)
C:\Users\me\AppDa…n\lib\Logger.js:190 09:03:31: JoplinServerApi: Could not acquire session: undefined 
 SyntaxError: Unexpected token < in JSON at position 0
    at JSON.parse (<anonymous>)
    at JoplinServerApi.<anonymous> (C:\Users\me\AppDa…ServerApi.js:200:42)
    at (<anonymous>)
    at C:\Users\me\AppDa…inServerApi.js:8:71
    at new Promise (<anonymous>)
    at __awaiter (C:\Users\me\AppDa…inServerApi.js:4:12)
    at loadResponseJson (C:\Users\me\AppDa…ServerApi.js:195:48)
    at JoplinServerApi.<anonymous> (C:\Users\me\AppDa…ServerApi.js:236:38)
    at (<anonymous>)
    at fulfilled (C:\Users\me\AppDa…inServerApi.js:5:58)
C:\Users\me\AppDa…n\lib\Logger.js:190 09:03:31: ShareService: Failed to run maintenance: SyntaxError: Unexpected token < in JSON at position 0
    at JSON.parse (<anonymous>)
    at JoplinServerApi.<anonymous> (C:\Users\me\AppDa…ServerApi.js:200:42)
    at (<anonymous>)
    at C:\Users\me\AppDa…inServerApi.js:8:71
    at new Promise (<anonymous>)
    at __awaiter (C:\Users\me\AppDa…inServerApi.js:4:12)
    at loadResponseJson (C:\Users\me\AppDa…ServerApi.js:195:48)
    at JoplinServerApi.<anonymous> (C:\Users\me\AppDa…ServerApi.js:236:38)
    at (<anonymous>)
    at fulfilled (C:\Users\me\AppDa…inServerApi.js:5:58)
C:\Users\me\AppDa…n\lib\Logger.js:190 09:03:31: Synchronizer: Sync: finished: Synchronisation finished [1678201388574]
C:\Users\me\AppDa…n\lib\Logger.js:190 09:03:31: Synchronizer: Operations completed: 
C:\Users\me\AppDa…n\lib\Logger.js:190 09:03:31: Synchronizer: Total folders: 1
C:\Users\me\AppDa…n\lib\Logger.js:190 09:03:31: Synchronizer: Total notes: 5
C:\Users\me\AppDa…n\lib\Logger.js:190 09:03:31: Synchronizer: Total resources: 3
C:\Users\me\AppDa…n\lib\Logger.js:190 09:03:31: Synchronizer: There was some errors:
C:\Users\me\AppDa…n\lib\Logger.js:190 09:03:31: Synchronizer: SyntaxError: Unexpected token < in JSON at position 0
    at JSON.parse (<anonymous>)
    at JoplinServerApi.<anonymous> (C:\Users\me\AppDa…ServerApi.js:200:42)
    at (<anonymous>)
    at C:\Users\me\AppDa…inServerApi.js:8:71
    at new Promise (<anonymous>)
    at __awaiter (C:\me\me\AppDa…inServerApi.js:4:12)
    at loadResponseJson (C:\Users\me\AppDa…ServerApi.js:195:48)
    at JoplinServerApi.<anonymous> (C:\Users\me\AppDa…ServerApi.js:236:38)
    at (<anonymous>)
    at fulfilled (C:\Users\me\AppDa…inServerApi.js:5:58)

Here are the logs. I tried to edit out personal information and hopefully I got it all.

I am starting to think it is on the cloudflare side. It might be getting stuck at the authentication page for the cloudflare tunnel and not hitting the actual joplin login page. I am using OAuth for my cloudflare tunnel.

That is what I was thinking however I do not have the knowledge to help you solve this...

Anyone else?

If you open https://yourserver/joplinserverurl/ping, what does it show?

Path not found: joplinserverurl/ping
Path not found: ping

The server page still pulls up. I am guessing that is the client side web portal maybe?

I'm running into the same issue here trying to access my selfhosted joplin server over the web via cloudflare zero trust. I'm auth'ed into cloudflare by WARP and can use the joplin address/web portal just fine, but the app itself won't sync. It just feeds the same error:
Unexpected token < in JSON at position 0

I am getting Last error: SyntaxError: Unexpected token H in JSON at position 0
Setup --> self-hosted Nextcloud sync

i can login to nextcloud and browse files.

i had issue with a file being locked. so i disabled locking and then got the unexpected token error. I am going to experiment more.

Im getting

DevTools failed to load source map:

This is the error I am getting: Problem moving sync location to new Nextcloud server - #3 by awilisch

It seems connected to this: Encryption header visible in newly created files · Issue #2102 · nextcloud/richdocuments · GitHub

And this: Moving of shared directory not working - #8 by devnull - ‚ĄĻÔłŹ Support - Nextcloud community

And here: Encryption Issue on Nextcloud 24 - #2 by yahesh - ‚ĄĻÔłŹ Support - Nextcloud community

Has anyone using Cloudflare made any progress on this? Evernote is raising my annual rate 42% from $34.95 to $49.95. This is the last straw so I've been moving over to Joplin. I installed Joplin Server running on docker accessible through Cloudflare. I have imported almost a dozen of my notebooks without a single issue - very nice! Everything is available in the desktop client. However, I just realized nothing is being synch'ed. When I try to sync using my external (Cloudflare) URL I get the error reported in this thread (Unexpected token < in JSON).

If I access the ping url ( it reports in the browser:

{"status":"ok","message":"Joplin Server is running"}

The ping does show up in my server logs:

2023-08-21 21:34:53: App: GET /joplin/ping (404) (1ms)
2023-08-21 21:35:05: App: GET /api/ping (200) (1ms)

However, none of the synchronization attempts show up in the server logs. If I use the curl command from the client dev tool, it appears to connect (though this connection doesn't show in the server log):

curl -v -X POST -H "X-API-MIN-VERSION: 2.6.0" -H "Content-Type: application/json" -H "Content-Length: 56" --data '{"email":"","password":"password"}' ''
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying
* Connected to ( port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server accepted to use h2
* Server certificate:
*  subject:
*  start date: Jul  5 18:48:42 2023 GMT
*  expire date: Oct  3 18:48:41 2023 GMT
*  subjectAltName: host "" matched cert's "*"
*  issuer: C=US; O=Google Trust Services LLC; CN=GTS CA 1P5
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x55eda6777300)
> POST /api/sessions HTTP/2
> Host:
> user-agent: curl/7.68.0
> accept: */*
> x-api-min-version: 2.6.0
> content-type: application/json
> content-length: 56
* We are completely uploaded and fine
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
* Connection state changed (MAX_CONCURRENT_STREAMS == 256)!
< HTTP/2 302 
< date: Mon, 21 Aug 2023 21:46:15 GMT
< location:
< set-cookie: CF_AppSession=n36ecdc54a2da7ba5; Expires=Tue, 22 Aug 2023 21:46:15 GMT; Path=/; Secure; HttpOnly
< access-control-allow-credentials: true
< cache-control: private, max-age=0, no-store, no-cache, must-revalidate, post-check=0, pre-check=0
< expires: Thu, 01 Jan 1970 00:00:01 GMT
< report-to: {"endpoints":[{"url":"https:\/\/\/report\/v3?s=J8o2mJHquONVLzvCPLw3OIvHdtwYm1pEIblMeVSJFQLDMvKmjCrqaU9p57YgtWQQyM%2F%2BrbOvb1lsWfP3BnE1srUnSMozcGFpfhJEV6gZ1ja1mBJ3%2BC2OAIbnV9mkyEpaI6TrM80%3D"}],"group":"cf-nel","max_age":604800}
< nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
< x-content-type-options: nosniff
< server: cloudflare
< cf-ray: 7fa61153d9f8ef7f-PDX
< alt-svc: h3=":443"; ma=86400
* Connection #0 to host left intact

Is this output not getting passed back from Cloudflare correctly? It seems this is where the < character is coming from but I'm not sure because I'm not sure what the output is supposed to look like.

Could anyone help with confirming if this output is wrong (additional adornments such as < and/or *)? Any other suggestions? Thanks for any suggestions!


Another thing to try is trying to fetch the sync target information from Joplin Desktop's development tools ‚ÄĒ the "Details" dropdown under this GitHub issue comment should describe how to do this:

Open the developer tools on one of the desktop clients and type the following into the console:

  1. Setting.value('')
    • This returns the ID of the sync target currently configured to be used with Joplin. This should be 9 for Joplin Server.
  2. const reg = require('@joplin/lib/registry').reg;
    • This imports a library that gives access to Joplin's synchronizer, logger, database, etc.
  3. const sync = await reg.syncTarget(9).synchronizer()
    • reg.syncTarget(9).synchronizer() returns the Joplin Server sync target (which has ID 9)
  4. await sync.api().get('info.json')
    • sync.api() returns the synchronizer's file API and .get('info.json') fetches info.json from the sync target.

Cool, I'll try this and post back. I did set this using the CLI but I get even less debug info from that platform so I went back to the desktop client.


I get the same response:


Is there anyway to see the plain text of the response that it's trying to parse as JSON?


It looks like the error is coming from this line:

Another thing to try is overriding JSON.parse with a version that logs a more detailed error message. For example,

const originalJSONParse = JSON.parse;
JSON.parse = function(data) {
    try {
        // Call the original JSON.parse with the same `this` varaible
        // and arguments.
        return originalJSONParse.apply(this, arguments);
    } catch (error) {
        console.warn('ERROR: Failed to parse', data);
        throw error;
1 Like

Thanks, @personalizedrefriger , that was definitely informative and a nice troubleshooting hack! The operative part of the output looks like this:

<!DOCTYPE html>
    <title>Sign in „ÉĽ Cloudflare Access</title>
    <meta charset="utf-8" />
    <meta name="robots" content="noindex" />
    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width" />
    <article id="data"
    <style>*{-webkit-box-sizing:inherit;box-sizing:inherit}body,html{min-height:100vh}html{background:#f7f7f8;text-align:center;text-rendering:optimizeLegibility;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";line-height:1.5;word-wrap:break-word;-webkit-box-sizing:border-box;box-sizing:border-box;background:#eeeeee;color:#333333}.Content,body{di


I did suspect/fear that it would come back to Cloudflare but because I had already authenticated through the Joplin admin console I thought it would carry over to the Joplin desktop client. Now it seems obvious that the web session through the admin page is its own session and the desktop client wants its own session (or Cloudflare demands it claims its own).

I think there's a way through Cloudflare where I can setup a rule to skip the authentication for a designated request header or some such thing, I'm not savvy with it.

Unfortunately (or fortunately, depending on your perspective), it does seem like the best way to handle it is through Cloudflare. But just out of curiosity since you are knowledgable of it: do you think it would be possible to patch in the session ID of the web admin login into the Joplin client? Or would the client setup a bunch of other things in its session that would be missing in the web login session?


I was able to setup a Bypass rule in Cloudflare based on my router's IP address for origin. It's not ideal but I think synchronizing only from home will cover 80% of my use cases. In the future I believe I can setup a Cloudflare Warp client (that has a mobile client) and could theoretically sync on the road as well. Thanks, @personalizedrefriger for your help!