{"record":{"id":"e723b7fd10f664af","repo":"linera-io/linera-protocol","slug":"this-user-already-has-a-chain","errorCode":null,"errorMessage":"This user already has a chain","messagePattern":"This user already has a chain","errorType":"validation","errorClass":"async_graphql::Error","httpStatus":null,"severity":"warning","filePath":"linera-faucet/server/src/lib.rs","lineNumber":866,"sourceCode":"            let now = self.client.storage_client().clock().current_time();\n            let period = current_daily_period(initial_claim.timestamp.micros(), now.micros());\n            let last_period = self\n                .faucet_storage\n                .last_daily_claim_period(&request.owner)\n                .await?\n                .unwrap_or(0);\n\n            if period <= last_period {\n                return Err(Error::new(DAILY_LIMIT_MSG));\n            }\n        } else {\n            match self.faucet_storage.get_chain_id(&request.owner).await {\n                Ok(None) => {}\n                Ok(Some(_)) => {\n                    // Not counted here: the requester side classifies this\n                    // refusal by message and counts it once as \"duplicate\"\n                    // (counting at both sites double-counted the request).\n                    return Err(Error::new(DUPLICATE_CHAIN_MSG));\n                }\n                Err(err) => {\n                    tracing::error!(\"Database error: {err}\");\n                    return Err(Error::new(err.to_string()));\n                }\n            }\n        }\n        Ok(())\n    }\n\n    /// Checks if the given requests can currently be fulfilled, based on the balance\n    /// and rate limiting settings. Returns an error if not.\n    async fn check_rate_limiting(&self, requests: &[PendingRequest]) -> async_graphql::Result<()> {\n        let end_timestamp = self.config.end_timestamp;\n        let start_timestamp = self.config.start_timestamp;\n        let local_time = self.client.storage_client().clock().current_time();\n        let full_duration = end_timestamp.delta_since(start_timestamp).as_micros();\n        let remaining_duration = end_timestamp.delta_since(local_time).as_micros();","sourceCodeStart":848,"sourceCodeEnd":884,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-faucet/server/src/lib.rs#L848-L884","documentation":"Returned by the faucet's batch validation when an initial `claim` request reaches execution although the owner already has a chain in faucet storage. The front-door check in `do_claim` normally makes `claim` idempotent — it returns the existing ChainDescription — so this message (`DUPLICATE_CHAIN_MSG`) only appears when a duplicate races past the front door (two concurrent claims for the same owner) and is refused during `validate_request`. The requester side classifies this message as 'duplicate' in metrics.","triggerScenarios":"Two concurrent `claim` mutations for the same AccountOwner submitted before the first one is recorded in the faucet database; retrying a claim whose response was lost after the chain was already created and stored.","commonSituations":"Double-submitted forms; client retry logic firing while the first request is still queued in the faucet batch processor; load tests reusing one owner key with parallel requests.","solutions":["Retry the `claim` mutation: with the chain now recorded, the front door returns the existing ChainDescription instead of erroring.","De-duplicate claim requests client-side — keep at most one in-flight claim per owner.","Treat this message as 'already claimed' and re-fetch the existing chain, not as a hard failure."],"exampleFix":"// before\nconst d1 = claim(owner); const d2 = claim(owner); // second may hit 'This user already has a chain'\n\n// after\nconst inFlight = new Map();\nfunction claimOnce(owner) {\n  if (!inFlight.has(owner)) inFlight.set(owner, faucet.claim(owner).finally(() => inFlight.delete(owner)));\n  return inFlight.get(owner);\n}","handlingStrategy":"fallback","validationCode":"const existing = await faucet.queryChainForOwner(owner); // if exposed\nif (existing) return existing; // skip claim entirely","typeGuard":null,"tryCatchPattern":"try { return await faucet.claim(owner); } catch (e) { if (/already has a chain/i.test(e.message)) return await faucet.claim(owner); /* front door now returns the existing description */ throw e; }","preventionTips":["Keep a single in-flight claim per owner (dedup map/promise cache).","Make claim idempotent client-side by checking local state before retrying.","Never fire parallel claims for the same key from multiple tabs/workers."],"tags":["faucet","duplicate","race-condition","claim","graphql"],"backgroundTag":"duplicate-resource-creation","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}