fix: escape special characters in Linear GraphQL queries (#13490)
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.
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user