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
- 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).
- Call
connectTo(WALLET_CONNECTORS.AUTH, { authConnection: AUTH_CONNECTION.DISCORD }) (or trigger the equivalent flow from the modal UI) from a Linux desktop browser.
- 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
- 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.
- 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.
Description
Social login via
AUTH_CONNECTION.DISCORD(through@web3auth/modal, Vue integration) fails consistently on Linux desktop with a400 Bad Requestfromhttps://api.web3auth.io/citadel-service/v1/auth/login, caused by an empty/missingosVersionfield 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:(Chromium-based browsers)
(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.osVersionarriving empty/undefined in the request built by the hosted login UI (served fromauth.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'sgetDeviceInfo(), which usesbowserand safely falls back toosVersion: "0"), and it is not the code path responsible — the hosted login page appears to build this payload independently, likely also viabowser, but without an equivalent fallback for OS versionsbowsercannot resolve.This is consistent with
bowser(and similar UA-parsing libraries) not returning anyos.versionfor 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
400response is received, something in the login/error-handling path appears to read the response body twice (e.g. once to checkres.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, informative400error behind a generic, confusing message.Steps to reproduce
@web3auth/modalwith@web3auth/modal/vue,WEB3AUTH_NETWORK.SAPPHIRE_DEVNET, EVM chain config (Base Sepolia in our case, though the chain is likely irrelevant to this bug).connectTo(WALLET_CONNECTORS.AUTH, { authConnection: AUTH_CONNECTION.DISCORD })(or trigger the equivalent flow from the modal UI) from a Linux desktop browser.POST https://api.web3auth.io/citadel-service/v1/auth/loginrequest fail with400, immediately followed by theResponse.clone/ "Body has already been consumed" error surfacing as the user-facing failure.Environment where it reproduces
vite build+vite preview/Netlify deploy), not justvite dev— rules out a dev-server-specific cause@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
AUTH_CONNECTION.DISCORDflow, only difference being the client OS reports a standard OS version string. On this environment,connectTo()resolves without the400/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
loginRecordpayload on the hosted login page, apply the same kind of safe fallback already used in@toruslabs/base-controllers'sgetDeviceInfo()(osVersion: o.version || "0") instead of leaving the field empty/undefined when the UA parser can't resolve an OS version.Response.clone: Body has already been consumedmessage should be fixed so that a failed login request surfaces its actual server-provided error (e.g. the400body 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.