From b41d2f6908499adee26f665fbeb76bcb38c627c8 Mon Sep 17 00:00:00 2001 From: RyanL1997 Date: Fri, 10 Jul 2026 04:38:02 +0000 Subject: [PATCH] Fix CSV/RAW output header being application/json rather than plain/text Backport of #1779 to 1.3. The SQL/PPL REST handlers hardcode the response Content-Type to "application/json" for every format, including format=csv and format=raw whose bodies are plain text. Downstream clients that trust the Content-Type header (e.g. OpenSearch Dashboards' legacy client used by Query Workbench) then attempt to JSON-parse the CSV/raw body, which fails and surfaces as an HTTP 400 to the user even though the data is correct. Add ResponseFormatter#contentType(): FlatResponseFormatter (CSV/RAW) returns "plain/text; charset=UTF-8" and JsonResponseFormatter (JDBC/ JSON) returns "application/json; charset=UTF-8". Thread the formatter's content type through RestSQLQueryAction so each format returns the correct header. JDBC/JSON responses are unchanged. Verified on a local OpenSearch 1.3.2 cluster: format=csv/raw now return plain/text (200) and the Query Workbench CSV/Text download tests pass (previously 400). --- .../sql/legacy/plugin/RestSQLQueryAction.java | 13 +++++++++---- .../response/format/FlatResponseFormatter.java | 6 ++++++ .../response/format/JsonResponseFormatter.java | 6 ++++++ .../protocol/response/format/ResponseFormatter.java | 7 +++++++ 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/legacy/src/main/java/org/opensearch/sql/legacy/plugin/RestSQLQueryAction.java b/legacy/src/main/java/org/opensearch/sql/legacy/plugin/RestSQLQueryAction.java index 51484feda7a..eb5dc0e80a0 100644 --- a/legacy/src/main/java/org/opensearch/sql/legacy/plugin/RestSQLQueryAction.java +++ b/legacy/src/main/java/org/opensearch/sql/legacy/plugin/RestSQLQueryAction.java @@ -167,14 +167,15 @@ private ResponseListener createQueryResponseListener(RestChannel @Override public void onResponse(QueryResponse response) { sendResponse(channel, OK, - formatter.format(new QueryResult(response.getSchema(), response.getResults()))); + formatter.format(new QueryResult(response.getSchema(), response.getResults())), + formatter.contentType()); } @Override public void onFailure(Exception e) { LOG.error("Error happened during query handling", e); logAndPublishMetrics(e); - sendResponse(channel, INTERNAL_SERVER_ERROR, formatter.format(e)); + sendResponse(channel, INTERNAL_SERVER_ERROR, formatter.format(e), formatter.contentType()); } }; } @@ -188,8 +189,12 @@ private T doPrivileged(PrivilegedExceptionAction action) { } private void sendResponse(RestChannel channel, RestStatus status, String content) { - channel.sendResponse(new BytesRestResponse( - status, "application/json; charset=UTF-8", content)); + sendResponse(channel, status, content, "application/json; charset=UTF-8"); + } + + private void sendResponse(RestChannel channel, RestStatus status, String content, + String contentType) { + channel.sendResponse(new BytesRestResponse(status, contentType, content)); } private static void logAndPublishMetrics(Exception e) { diff --git a/protocol/src/main/java/org/opensearch/sql/protocol/response/format/FlatResponseFormatter.java b/protocol/src/main/java/org/opensearch/sql/protocol/response/format/FlatResponseFormatter.java index d4d00c45356..562e62c5902 100644 --- a/protocol/src/main/java/org/opensearch/sql/protocol/response/format/FlatResponseFormatter.java +++ b/protocol/src/main/java/org/opensearch/sql/protocol/response/format/FlatResponseFormatter.java @@ -19,6 +19,8 @@ @RequiredArgsConstructor public abstract class FlatResponseFormatter implements ResponseFormatter { + public static final String CONTENT_TYPE = "plain/text; charset=UTF-8"; + private static String INLINE_SEPARATOR = ","; private static final String INTERLINE_SEPARATOR = System.lineSeparator(); private static final Set SENSITIVE_CHAR = ImmutableSet.of("=", "+", "-", "@"); @@ -30,6 +32,10 @@ public FlatResponseFormatter(String seperator, boolean sanitize) { this.sanitize = sanitize; } + public String contentType() { + return CONTENT_TYPE; + } + @Override public String format(QueryResult response) { FlatResult result = new FlatResult(response, sanitize); diff --git a/protocol/src/main/java/org/opensearch/sql/protocol/response/format/JsonResponseFormatter.java b/protocol/src/main/java/org/opensearch/sql/protocol/response/format/JsonResponseFormatter.java index 03e060925de..baf26da8ea9 100644 --- a/protocol/src/main/java/org/opensearch/sql/protocol/response/format/JsonResponseFormatter.java +++ b/protocol/src/main/java/org/opensearch/sql/protocol/response/format/JsonResponseFormatter.java @@ -24,6 +24,8 @@ @RequiredArgsConstructor public abstract class JsonResponseFormatter implements ResponseFormatter { + public static final String CONTENT_TYPE = "application/json; charset=UTF-8"; + /** * JSON format styles: pretty format or compact format without indent and space. */ @@ -36,6 +38,10 @@ public enum Style { */ private final Style style; + public String contentType() { + return CONTENT_TYPE; + } + @Override public String format(R response) { return jsonify(buildJsonObject(response)); diff --git a/protocol/src/main/java/org/opensearch/sql/protocol/response/format/ResponseFormatter.java b/protocol/src/main/java/org/opensearch/sql/protocol/response/format/ResponseFormatter.java index 4618a4f80d2..834a579b50b 100644 --- a/protocol/src/main/java/org/opensearch/sql/protocol/response/format/ResponseFormatter.java +++ b/protocol/src/main/java/org/opensearch/sql/protocol/response/format/ResponseFormatter.java @@ -27,4 +27,11 @@ public interface ResponseFormatter { */ String format(Throwable t); + /** + * The contentType of the formatter. + * + * @return string with the contentType of the formatter + */ + String contentType(); + }