Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/logflare_web/controllers/endpoints_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ defmodule LogflareWeb.EndpointsController do
use LogflareWeb, :controller
use OpenApiSpex.ControllerSpecs

import Logflare.Utils.Guards, only: [is_non_empty_binary: 1]

require Logger

alias Logflare.Backends.QueryError
Expand Down Expand Up @@ -97,6 +99,9 @@ defmodule LogflareWeb.EndpointsController do
{:error, error = %QueryError{}} ->
render(conn, "query.json", error: QueryErrorHelpers.query_error_message(error))

{:error, message} when is_non_empty_binary(message) ->
render(conn, "query.json", error: QueryErrorHelpers.sandbox_query_error_message(message))

{:error, _errors} ->
render(conn, "query.json", error: QueryErrorHelpers.generic_query_error_message())
end
Expand Down
37 changes: 37 additions & 0 deletions lib/logflare_web/query_error_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,43 @@ defmodule LogflareWeb.QueryErrorHelpers do
@spec generic_query_error_message() :: String.t()
def generic_query_error_message, do: @generic_query_error_message

@doc """
Returns a user-facing message for a sandboxed-query validation error, as
returned by `Logflare.Endpoints.run_query/3` when the query fails validation
before ever reaching a backend.

Only the "unknown table" case is translated into a plain message; anything
else falls back to the generic message, since Logflare's internal CTE
splicing would otherwise leak into a user-facing error and confuse callers
who never wrote a CTE themselves.

iex> LogflareWeb.QueryErrorHelpers.sandbox_query_error_message("Table not found in CTE: (function_logs)")
~s(Table "function_logs" does not exist.)

iex> LogflareWeb.QueryErrorHelpers.sandbox_query_error_message("Multiple CTEs available (first_cte, second_cte). You must specify which one to query using `f:name`")
"Backend error! Retry your query. Please contact support if this continues."

iex> LogflareWeb.QueryErrorHelpers.sandbox_query_error_message("sql parser error: Expected: SELECT, VALUES, or a subquery in the query body, found: EOF")
"Backend error! Retry your query. Please contact support if this continues."
"""
@spec sandbox_query_error_message(String.t()) :: String.t()
def sandbox_query_error_message(message) when is_binary(message) do
classified_sandbox_error_message(message) || generic_query_error_message()
end

defp classified_sandbox_error_message("Table not found in CTE: (" <> rest) do
rest |> String.trim_trailing(")") |> unknown_table_message()
end

defp classified_sandbox_error_message(_message), do: nil

defp unknown_table_message(names) do
case String.split(names, ", ") do
[single] -> ~s(Table "#{single}" does not exist.)
multiple -> "Tables #{Enum.map_join(multiple, ", ", &~s("#{&1}"))} do not exist."
end
end

@doc """
Whether a backend %QueryError{} was caused by a query timeout.

Expand Down
33 changes: 29 additions & 4 deletions test/logflare_web/controllers/endpoints_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,7 @@ defmodule LogflareWeb.EndpointsControllerTest do
conn
|> json_response(200)

assert response["error"] =~
LogflareWeb.QueryErrorHelpers.generic_query_error_message()
assert response["error"] == LogflareWeb.QueryErrorHelpers.generic_query_error_message()

refute response["result"]
end
Expand Down Expand Up @@ -376,8 +375,7 @@ defmodule LogflareWeb.EndpointsControllerTest do
conn
|> json_response(200)

assert response["error"] =~
LogflareWeb.QueryErrorHelpers.generic_query_error_message()
assert response["error"] == LogflareWeb.QueryErrorHelpers.generic_query_error_message()

refute response["result"]
end
Expand Down Expand Up @@ -503,6 +501,33 @@ defmodule LogflareWeb.EndpointsControllerTest do
assert String.downcase(sql) =~ "select 2"
end

test "sql referencing a table not in the endpoint's CTE returns a specific error", %{
conn: conn,
user: user
} do
reject(&BigQueryJobs.bigquery_jobs_query/3)

backend = insert(:backend, type: :clickhouse, user: user)

endpoint =
insert(:endpoint,
user: user,
backend: backend,
language: :ch_sql,
enable_auth: true,
sandboxable: true,
query: "with a as (select 1 as b) select b from a"
)

conn =
conn
|> put_req_header("x-api-key", user.api_key)
|> get(~p"/api/endpoints/query/#{endpoint.name}", %{sql: "select b from function_logs"})

assert json_response(conn, 200)["error"] == ~s(Table "function_logs" does not exist.)
refute json_response(conn, 200)["result"]
end

test "LQL params in GET query string", %{conn: conn, user: user} do
pid = self()

Expand Down
Loading