From 67112647e81f9fbdedf168f87c6058aabef7a60b Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 9 Feb 2026 16:18:04 +0530 Subject: [PATCH] fix: escape special characters in Linear GraphQL queries (#13490) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creating a Linear issue from Chatwoot fails with a GraphQL parse error when the title, description, or search term contains double quotes. For example, a description like `the sender is "Bot"` produces this broken query: ```graphql issueCreate(input: { description: "the sender is "Bot"" }) ``` Linear's API rejects this with `Syntax Error: Expected ":", found String`. This affects issue creation, issue linking, and issue search — any flow where user-provided text is interpolated into a GraphQL query. The `graphql_value` helper was only escaping newlines (`\n`) but not quotes, backslashes, or other characters that are meaningful inside a GraphQL string literal. On top of that, `issue_link` and `search_issue` bypassed `graphql_value` entirely, using raw string interpolation instead. The fix replaces the manual `gsub` escaping with Ruby's `to_json`, which produces a properly escaped, double-quoted string that handles all special characters. This is a minimal, well-understood substitution — `to_json` on a Ruby string returns a valid JSON string literal, which is also a valid GraphQL string literal since GraphQL uses the same escaping rules. The `issue_link` mutation and `search_issue` query are updated to route their parameters through `graphql_value` instead of raw interpolation. The `team_entities_query` and `linked_issues` methods in `queries.rb` also use raw interpolation, but their inputs are system-generated IDs and URLs rather than user-provided text, so they're left as-is to keep this change focused. --- lib/linear/mutations.rb | 7 ++---- lib/linear/queries.rb | 2 +- spec/lib/linear_spec.rb | 48 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/lib/linear/mutations.rb b/lib/linear/mutations.rb index 5f34e602b..1935ba478 100644 --- a/lib/linear/mutations.rb +++ b/lib/linear/mutations.rb @@ -2,13 +2,10 @@ module Linear::Mutations def self.graphql_value(value) case value when String - # Strings must be enclosed in double quotes - "\"#{value.gsub("\n", '\\n')}\"" + value.to_json when Array - # Arrays need to be recursively converted "[#{value.map { |v| graphql_value(v) }.join(', ')}]" else - # Other types (numbers, booleans) can be directly converted to strings value.to_s end end @@ -47,7 +44,7 @@ module Linear::Mutations <<~GRAPHQL mutation { - attachmentLinkURL(url: "#{link}", issueId: "#{issue_id}", title: "#{title}"#{user_params_str}) { + attachmentLinkURL(url: #{graphql_value(link)}, issueId: #{graphql_value(issue_id)}, title: #{graphql_value(title)}#{user_params_str}) { success attachment { id diff --git a/lib/linear/queries.rb b/lib/linear/queries.rb index 54daaf30c..cc705625e 100644 --- a/lib/linear/queries.rb +++ b/lib/linear/queries.rb @@ -48,7 +48,7 @@ module Linear::Queries def self.search_issue(term) <<~GRAPHQL query { - searchIssues(term: "#{term}") { + searchIssues(term: #{Linear::Mutations.graphql_value(term)}) { nodes { id title diff --git a/spec/lib/linear_spec.rb b/spec/lib/linear_spec.rb index e15f64382..65c7cb664 100644 --- a/spec/lib/linear_spec.rb +++ b/spec/lib/linear_spec.rb @@ -93,6 +93,30 @@ describe Linear do end let(:user) { instance_double(User, name: 'John Doe', avatar_url: 'https://example.com/avatar.jpg') } + context 'when description contains double quotes' do + it 'produces valid GraphQL by escaping the quotes' do + allow(linear_client).to receive(:post) do |payload| + expect(payload[:query]).to include('description: "the sender is \\"Bot\\"') + instance_double(HTTParty::Response, success?: true, + parsed_response: { 'data' => { 'issueCreate' => { 'id' => 'issue1', 'title' => 'Title' } } }) + end + + linear_client.create_issue(params.merge(description: 'the sender is "Bot"')) + end + end + + context 'when description contains backslashes' do + it 'produces valid GraphQL by escaping the backslashes' do + allow(linear_client).to receive(:post) do |payload| + expect(payload[:query]).to include('description: "path\\\\to\\\\file"') + instance_double(HTTParty::Response, success?: true, + parsed_response: { 'data' => { 'issueCreate' => { 'id' => 'issue1', 'title' => 'Title' } } }) + end + + linear_client.create_issue(params.merge(description: 'path\\to\\file')) + end + end + context 'when the API response is success' do before do stub_request(:post, url) @@ -213,6 +237,18 @@ describe Linear do let(:title) { 'Title' } let(:user) { instance_double(User, name: 'John Doe', avatar_url: 'https://example.com/avatar.jpg') } + context 'when title contains double quotes' do + it 'produces valid GraphQL by escaping the quotes' do + allow(linear_client).to receive(:post) do |payload| + expect(payload[:query]).to include('title: "say \\"hello\\"') + instance_double(HTTParty::Response, success?: true, + parsed_response: { 'data' => { 'attachmentLinkURL' => { 'id' => 'attachment1' } } }) + end + + linear_client.link_issue(link, issue_id, 'say "hello"') + end + end + context 'when the API response is success' do before do stub_request(:post, url) @@ -332,6 +368,18 @@ describe Linear do context 'when querying issues' do let(:term) { 'term' } + context 'when search term contains double quotes' do + it 'produces valid GraphQL by escaping the quotes' do + allow(linear_client).to receive(:post) do |payload| + expect(payload[:query]).to include('term: "find \\"Bot\\"') + instance_double(HTTParty::Response, success?: true, + parsed_response: { 'data' => { 'searchIssues' => { 'nodes' => [] } } }) + end + + linear_client.search_issue('find "Bot"') + end + end + context 'when the API response is success' do before do stub_request(:post, url)