Capacitor Sending Two Cookies - One Valid, One Not - on Both IOS and Android

TLDR; Capacitor appears to be sending two cookies with every request on both IOS and Android, one valid and one not.

–———

I’ve been using Capacitor to generate mobile apps from a web platform for both IOS and Android. The web platform is a Node.js (24.5.0) and Express (v5) app with a React (19) and Redux (9) frontend. I’m using Capacitor 7.4.3.

Here’s npx cap doctor output:

💊   Capacitor Doctor  💊

Latest Dependencies:

  @capacitor/cli: 7.4.3
  @capacitor/core: 7.4.3
  @capacitor/android: 7.4.3
  @capacitor/ios: 7.4.3

Installed Dependencies:

  @capacitor/cli: 7.4.3
  @capacitor/core: 7.4.3
  @capacitor/android: 7.4.3
  @capacitor/ios: 7.4.3

[success] iOS looking great! 👌
[success] Android looking great! 👌 

Here’s npx cap ls

[info] Found 4 Capacitor plugins for android:
       @capacitor/app@7.1.0
       @capacitor/device@7.0.2
       @capacitor/push-notifications@7.0.2
       @capgo/capacitor-updater@7.8.7
[info] Found 4 Capacitor plugins for ios:
       @capacitor/app@7.1.0
       @capacitor/device@7.0.2
       @capacitor/push-notifications@7.0.2
       @capgo/capacitor-updater@7.8.7
[info] Listing plugins for web is not possible.

Here’s my capacitor config:

const config = {
  appId: "<REDACTED: myapp>",
  appName: "<REDACTED: myapp>",
  webDir: "web-application/public/dist/",
  plugins: {
    CapacitorHttp: {
      enabled: true
    },
    CapacitorUpdater: {
      autoUpdate: false,
      keepUrlPathAfterReload: true
    },
    CapacitorCookies: {
      enabled: true
    }
  }
}

The app is mostly a pretty straight-forward REST API, but I added a websocket recently to handle live updating notifications. Authentication is through Cookies using express-session and it uses express-cors to handle CORS headers.

I first noticed the issue when I attempted to deploy the capacitor release to a staging environment in preparation for getting the apps on real devices. Up until then I’d been developing on local and testing through the device simulators. Everything appeared to be work on the device simulators, which I now suspect was because the API domain on my local was none other than localhost:3000.

As soon as I pointed the simulators at staging.mydomainthings started flaking.

The first thing to go was authentication in the websocket. I use express-session to authenticate the websocket connection and attach it to the user’s session, but Capacitor wasn’t sending the cookies in the HTTP Upgrade request. For either Android or IOS.

Then I noticed that Android was being inconsistent in whether it would hold on to the session or lose it, eventually it settled into just losing the session. On IOS the regular session seemed to still work.

I eventually discovered that Capacitor appears to be sending two cookies with each request:

cookie: ‘mycookie_id=[REDACTED]fuAc; AWSALB=[REDACTED]; AWSALBCORS=[REDACTED]; mycookie_id=[REDACTED]VEE’ 

It’s doing this on both IOS and Android. Only one of those cookies appears to actually be set by the server in a response header. I have no idea where the other one is coming from. In IOS, the cookie set by the server is returned first, and so the session works – Express parses and uses that cookie and ignores the second one. In Android, the reverse is true (most of the time – every 100th login or so it will randomly appear to work).

Originally, I was just using CapacitorHttpwhich I added to try to get around some CORS issues (I load image files from S3 using an authenticated URL and that wasn’t working in IOS on local). I added CapacitorCookies in an attempt to resolve the cookie errors, but no dice.

I’ve tried just about every permutation of Cookie settings to try to make the extra cookie go away:

  • SameSite: ‘none’, SameSite: ‘lax’, SameSite: ‘strict’
  • domain: mystagingdomain, domain: <not set>
  • secure: true, secure: <not set>

No luck.
I’ve got CORS set up to allow both capacitor://localhost and https://localhost on the theory that might be causing issues, but it doesn’t seem to have resolved the problem.

Here’s the CORS setup:

    app.use(cors({
        origin: [ core.config.host, 'capacitor://localhost', 'https://localhost' ],
        methods: [ 'GET', 'POST', 'PATCH', 'DELETE' ],
        allowedHeaders: [ 'Content-Type', 'Accept', 'X-CSRF-Token' ]
    }))

Here’s the current session configuration:

    const sessionParser = session({
        key: core.config.session.key, // mycookie_id
        secret: core.config.session.secret,
        store: sessionStore,
        resave: false,
        saveUninitialized: true,
        proxy: true,
        cookie: { 
            domain: new URL('/', core.config.host).hostname,
            path: '/',
            httpOnly: true,
            secure: true,
            sameSite: "strict",
            maxAge: 1000 * 60 * 60 * 24 * 30 // 30 days 
        } 
    })

The Capacitor WebView setup appears to run a local proxy on the device (the localhost that the app runs on and which loads the bundle from the local device) – I can see it referenced in a refererheader – my best guess is that’s where the cookie is coming from.

Interestingly, when I inspect the requests sent from the IOS simulator using the Safari inspector, I don’t see any cookies being sent at all. Here’s an example:

Request
Accept: application/json
Referer: capacitor://localhost/
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148

Here’s what the server got for that request:

2025-09-08T21:12:19.616Z debug :: Headers:  {
  'x-forwarded-for': '[REDACTED]',
  'x-forwarded-proto': 'https',
  'x-forwarded-port': '443',
  host: 'staging.[REDACTED: mydomain]',
  'x-amzn-trace-id': 'Root=[REDACTED]',
  accept: 'application/json',
  'accept-encoding': 'gzip, deflate, br',
  'if-none-match': 'W/"[REDACTED]"',
  'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148',
  'accept-language': 'en-US,en;q=0.9',
  referer: 'capacitor://localhost/',
  cookie: 'AWSALB=[REDACTED]; AWSALBCORS=[REDACTED]; mycookie_id=[REDACTED]fuAc; mycookie_id=[REDACTED]VEE'
}

I think I can work around it by hacking express-session to check both cookies and use the valid one, and I can potentially come up with an alternate auth method for the websocket, but I a) wanted to report the issue in case it was an unknown bug and b) wanted to check to see whether there was a simple method for eliminating the duplicate cookie I just haven’t discovered yet.

Is there a simple way to eliminate the duplicate cookie? Is there a way to get Capacitor to send its cookies with the HTTP Upgrade request to the API?

Additional things I tried yesterday:

Turning off CapacitorHttp and CapacitorCookies with pretty much every possible permutation of CORS settings and fetch settings, including credentials: ‘include’ and mode: ‘cors’. I couldn’t get the cookies to stick at all with CapacitorHttp and CapacitorCookies off.

With them back on, there are always two cookies. Again, I’ve tried just about every permutation of CORs, fetch and cookie settings at this point. I say “just about” because there are enough permutations that I can’t help feeling like I just haven’t found the right one yet…