{"record":{"id":"acdc04586f66f609","repo":"abhigyanpatwari/GitNexus","slug":"write-queries-are-not-allowed-via-the-http-api","errorCode":null,"errorMessage":"Write queries are not allowed via the HTTP API","messagePattern":"Write queries are not allowed via the HTTP API","errorType":"http","errorClass":null,"httpStatus":403,"severity":"error","filePath":"gitnexus/src/server/api.ts","lineNumber":674,"sourceCode":"      res.status(400).json({\n        error: '\"params\" must be a plain object with scalar values (string/number/boolean/null)',\n      });\n      return;\n    }\n\n    const entry = await resolveRepo(requestedRepo(req));\n    if (!entry) {\n      res.status(404).json({ error: 'Repository not found' });\n      return;\n    }\n    const lbugPath = path.join(entry.storagePath, 'lbug');\n    const result = await withLbugDb(lbugPath, () => executePrepared(cypher, queryParams ?? {}), {\n      readOnly: true,\n    });\n    res.json({ result });\n  } catch (err: any) {\n    if (isReadOnlyDbError(err)) {\n      res.status(403).json({ error: 'Write queries are not allowed via the HTTP API' });\n      return;\n    }\n    res.status(500).json({ error: err.message || 'Query failed' });\n  }\n};\n\n/**\n * Validate the optional `token` field of POST /api/analyze. Returns an\n * { status, error } to send, or null when the token is absent or valid.\n *\n * The token is a GitHub PAT: charset-restricted (blocks CRLF header\n * smuggling), length-bounded (1–256), and bound to github.com using the SAME\n * GITHUB_TOKEN_HOSTS allowlist + hostname parse as resolveGitCredential, so a\n * token the API accepts is exactly the one buildGitEnv will inject — and one\n * it rejects is never sent off github.com.\n *\n * Exported for unit tests (the route validation is otherwise only reachable\n * by booting the server).","sourceCodeStart":656,"sourceCodeEnd":692,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/aac7515d2a8c50a1f8f923c6fb77218b333560d6/gitnexus/src/server/api.ts#L656-L692","documentation":"HTTP 403 from POST /api/query when the Cypher statement attempts a write and the read-only LadybugDB connection rejects it — the catch inspects the error with isReadOnlyDbError. The HTTP API deliberately opens withLbugDb in readOnly: true mode, so the endpoint is a query surface only; any CREATE/MERGE/DELETE/SET/REMOVE/DETACH DELETE/DROP-style clause surfaces as this 403.","triggerScenarios":"MATCH (n) MERGE (n:X) RETURN n; MATCH (n) DELETE n; SET/REMOVE property writes; CALL of a subquery containing writes; maintaining Cypher that worked against a writable Neo4j and reusing it verbatim against the HTTP API.","commonSituations":"Trying to clean up or annotate graph nodes through the serve API instead of the CLI; porting admin Cypher scripts to the web client; experiments meant for a local writable console pasted into /api/query; misunderstandings where users expect the API to mutate the index.","solutions":["Rewrite the statement as read-only: MATCH/WITH/UNWIND/WHERE/RETURN only","Perform any write/maintenance through the gitnexus CLI (which owns index mutation) rather than the HTTP API","If a mutation genuinely belongs in the product, request/expect a dedicated authenticated admin endpoint — do not try to bypass the read-only flag","Check nested CALL { ... } blocks for hidden writes — the read-only rejection applies to the whole transaction"],"exampleFix":"// before\nbody: JSON.stringify({ cypher: 'MATCH (n:Symbol) SET n.seen = true RETURN n' }) // 403\n\n// after\nbody: JSON.stringify({ cypher: 'MATCH (n:Symbol) RETURN n.name LIMIT 25' }) // read-only is allowed","handlingStrategy":"validation","validationCode":"// Reject write-shaped Cypher before it reaches the API.\nconst WRITE_CLAUSE = /\\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|LOAD\\\\s+CSV|FOREACH)\\b/i;\nfunction assertReadOnlyCypher(cypher: string): void {\n  // strip string literals so keywords inside quotes don't false-positive\n  const stripped = cypher.replace(/'[^']*'|\\\"[^\\\"]*\\\"/g, '');\n  if (WRITE_CLAUSE.test(stripped)) throw new Error('HTTP query API is read-only — remove write clauses');\n}","typeGuard":null,"tryCatchPattern":"const res = await postQuery(cypher);\nif (res.status === 403 && /not allowed/.test((await res.json()).error)) {\n  throw new Error('this query writes — use the gitnexus CLI for index mutations');\n}","preventionTips":["Treat the HTTP query API as strictly read-only by design","Keep a separate writable workflow (CLI) for any graph maintenance","Audit nested CALL { ... } blocks for hidden writes"],"tags":["http-403","cypher","read-only","api","database"],"backgroundTag":"read-only-db-write-denied","analyzedSha":"aac7515d2a8c50a1f8f923c6fb77218b333560d6","analyzedAt":"2026-08-20T23:29:22.980Z","schemaVersion":2},"datasetVersion":"2026-08-22T14:17:55.899Z"}