Skip to content

Discord (social) login fails on Linux desktop — empty "loginRecord.osVersion" causes 400, masked by a secondary Response.clone crash #2537

Description

@CyrilHskt

Description

Social login via AUTH_CONNECTION.DISCORD (through @web3auth/modal, Vue integration) fails consistently on Linux desktop with a 400 Bad Request from https://api.web3auth.io/citadel-service/v1/auth/login, caused by an empty/missing osVersion field in the device-info payload sent by the hosted login page. The failed request then triggers a secondary, misleading error in the SDK's own error-handling path (Response.clone() called on an already-consumed body), which surfaces to the end user as:

Failed to connect with wallet. Failed to execute 'clone' on 'Response': Response body is already used

(Chromium-based browsers)

Failed to connect with wallet. Response.clone: Body has already been consumed.

(Firefox)

Root cause (as far as I could trace it client-side)

The actual rejection comes from the server, not the clone bug:

{
  "message": [
    "loginRecord.osVersion should not be empty",
    "loginRecord.osVersion must be a string"
  ],
  "error": "Bad Request",
  "statusCode": 400
}

This points to loginRecord.osVersion arriving empty/undefined in the request built by the hosted login UI (served from auth.web3auth.io, loaded dynamically — not part of the npm package, so I could not inspect that specific source directly). I did check the npm-published device-detection code that IS available locally (@toruslabs/base-controllers's getDeviceInfo(), which uses bowser and safely falls back to osVersion: "0"), and it is not the code path responsible — the hosted login page appears to build this payload independently, likely also via bowser, but without an equivalent fallback for OS versions bowser cannot resolve.

This is consistent with bowser (and similar UA-parsing libraries) not returning any os.version for Linux desktop user agents, since Linux distributions don't expose a standardized OS version the way Windows/macOS/iOS/Android do.

The clone error is a secondary bug in the SDK's own error-handling logic: when the 400 response is received, something in the login/error-handling path appears to read the response body twice (e.g. once to check res.ok, once to build the thrown error from the body), and the second .clone()/read call throws because the body stream was already consumed on the first read. This masks the real, informative 400 error behind a generic, confusing message.

Steps to reproduce

  1. Use @web3auth/modal with @web3auth/modal/vue, WEB3AUTH_NETWORK.SAPPHIRE_DEVNET, EVM chain config (Base Sepolia in our case, though the chain is likely irrelevant to this bug).
  2. Call connectTo(WALLET_CONNECTORS.AUTH, { authConnection: AUTH_CONNECTION.DISCORD }) (or trigger the equivalent flow from the modal UI) from a Linux desktop browser.
  3. Observe the POST https://api.web3auth.io/citadel-service/v1/auth/login request fail with 400, immediately followed by the Response.clone / "Body has already been consumed" error surfacing as the user-facing failure.

Environment where it reproduces

  • OS: Linux Mint 22.2 (Zara), kernel 6.17.0, x86_64 — Ubuntu/Debian-based
  • Browsers tested, all reproduce identically: Chromium (regular and private/incognito windows), Firefox (private window)
  • Also reproduces on a genuine production build (vite build + vite preview/Netlify deploy), not just vite dev — rules out a dev-server-specific cause
  • Packages: @web3auth/modal@11.3.0, @web3auth/auth@11.8.2, @web3auth/no-modal@11.3.0 (transitive), @toruslabs/base-controllers@9.12.0 (transitive)

Environment where it does NOT reproduce

  • Android (Brave browser) — same Client ID, same Sapphire Devnet project, same AUTH_CONNECTION.DISCORD flow, only difference being the client OS reports a standard OS version string. On this environment, connectTo() resolves without the 400/clone error entirely, confirming the failure is specific to how the login page detects OS version on Linux desktop UAs, not something in our app configuration, network, or Discord app setup.

Suggested fix

  1. In whatever code builds the loginRecord payload on the hosted login page, apply the same kind of safe fallback already used in @toruslabs/base-controllers's getDeviceInfo() (osVersion: o.version || "0") instead of leaving the field empty/undefined when the UA parser can't resolve an OS version.
  2. Independently of (1), the error-handling path that produces the Response.clone: Body has already been consumed message should be fixed so that a failed login request surfaces its actual server-provided error (e.g. the 400 body message) to the calling application instead of a secondary, unrelated exception. This would have made the real cause immediately visible instead of requiring packet-level inspection to diagnose.

Impact

Any Discord (and likely other social/auth AUTH_CONNECTION) login attempted from a Linux desktop browser currently fails outright, with a confusing, non-actionable error message that gives no indication of the real (OS-version-validation) cause.

Happy to provide additional logs, HAR file, or a minimal repro repository if useful.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions