Skip to content

Commit f6b6baa

Browse files
authored
[Enhancement] Reject unsupported output formats on the analytics-engine route with a 4xx (opensearch-project#5570)
Signed-off-by: Jialiang Liang <jiallian@amazon.com>
1 parent 5aa6ea0 commit f6b6baa

4 files changed

Lines changed: 125 additions & 0 deletions

File tree

plugin/src/main/java/org/opensearch/sql/plugin/SQLPlugin.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
import org.opensearch.sql.opensearch.storage.script.CompoundedScriptEngine;
108108
import org.opensearch.sql.plugin.config.EngineExtensionsHolder;
109109
import org.opensearch.sql.plugin.config.OpenSearchPluginModule;
110+
import org.opensearch.sql.plugin.rest.AnalyticsEngineFormatSupport;
110111
import org.opensearch.sql.plugin.rest.AnalyticsExecutorHolder;
111112
import org.opensearch.sql.plugin.rest.RestPPLGrammarAction;
112113
import org.opensearch.sql.plugin.rest.RestPPLQueryAction;
@@ -271,6 +272,13 @@ public void onFailure(Exception e) {
271272
}
272273
});
273274
} else {
275+
// Analytics route only emits JSON; reject unsupported formats (e.g. csv) with a 4xx.
276+
try {
277+
AnalyticsEngineFormatSupport.validateFormat(sqlRequest.format());
278+
} catch (Exception e) {
279+
RestSqlAction.handleException(channel, e);
280+
return true;
281+
}
274282
unifiedQueryHandler.execute(
275283
sqlRequest.getQuery(),
276284
QueryType.SQL,
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.plugin.rest;
7+
8+
import java.util.Locale;
9+
import org.opensearch.sql.common.error.ErrorCode;
10+
import org.opensearch.sql.common.error.ErrorReport;
11+
import org.opensearch.sql.common.error.QueryProcessingStage;
12+
import org.opensearch.sql.protocol.response.format.Format;
13+
14+
/**
15+
* Guards the analytics-engine query route, which only produces JSON. Rejects the alternate output
16+
* formats (csv/raw/viz) the route does not implement, instead of silently answering with JSON.
17+
*/
18+
public final class AnalyticsEngineFormatSupport {
19+
20+
private AnalyticsEngineFormatSupport() {}
21+
22+
/**
23+
* Throw an {@link ErrorReport} if the requested output format is unsupported on the analytics
24+
* engine. JSON/JDBC (the default) is supported; csv/raw/viz are not.
25+
*/
26+
public static void validateFormat(Format format) {
27+
// JDBC (the default) is the JSON contract the analytics route emits.
28+
if (format == Format.JDBC) {
29+
return;
30+
}
31+
throw ErrorReport.wrap(
32+
new IllegalArgumentException(
33+
String.format(
34+
Locale.ROOT,
35+
"response in %s format is not supported on the analytics engine",
36+
format.getFormatName())))
37+
.code(ErrorCode.UNSUPPORTED_OPERATION)
38+
.stage(QueryProcessingStage.ANALYZING)
39+
.location("while selecting the response format for the analytics engine")
40+
.suggestion(
41+
"The analytics engine only returns JSON. Remove the 'format' parameter, or run this"
42+
+ " query against a non-analytics index to use the requested format.")
43+
.build();
44+
}
45+
}

plugin/src/main/java/org/opensearch/sql/plugin/transport/TransportPPLQueryAction.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.opensearch.sql.opensearch.setting.OpenSearchSettings;
4141
import org.opensearch.sql.plugin.config.EngineExtensionsHolder;
4242
import org.opensearch.sql.plugin.config.OpenSearchPluginModule;
43+
import org.opensearch.sql.plugin.rest.AnalyticsEngineFormatSupport;
4344
import org.opensearch.sql.plugin.rest.AnalyticsExecutorHolder;
4445
import org.opensearch.sql.plugin.rest.RestUnifiedQueryAction;
4546
import org.opensearch.sql.ppl.PPLService;
@@ -185,6 +186,13 @@ protected void doExecute(
185186
task,
186187
createExplainResponseListener(transformedRequest, clearingListener));
187188
} else {
189+
// Analytics route only emits JSON; reject unsupported formats (e.g. csv) with a 4xx.
190+
try {
191+
AnalyticsEngineFormatSupport.validateFormat(format(transformedRequest));
192+
} catch (Exception e) {
193+
clearingListener.onFailure(e);
194+
return;
195+
}
188196
unifiedQueryHandler.execute(
189197
transformedRequest.getRequest(),
190198
QueryType.PPL,
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.plugin.rest;
7+
8+
import static org.junit.Assert.assertEquals;
9+
import static org.junit.Assert.assertNotNull;
10+
import static org.junit.Assert.assertTrue;
11+
12+
import org.junit.Test;
13+
import org.opensearch.sql.common.error.ErrorCode;
14+
import org.opensearch.sql.common.error.ErrorReport;
15+
import org.opensearch.sql.protocol.response.format.Format;
16+
17+
/**
18+
* Verifies {@link AnalyticsEngineFormatSupport#validateFormat(Format)} accepts the JSON/JDBC
19+
* default the analytics route emits and rejects unsupported output formats (csv/raw/viz) with a
20+
* structured {@link ErrorReport} so the REST layer returns a clean 4xx instead of silently
21+
* returning JSON.
22+
*/
23+
public class AnalyticsEngineFormatSupportTest {
24+
25+
@Test
26+
public void jdbcFormatIsAccepted() {
27+
// No exception expected — JDBC is the JSON contract the analytics route emits.
28+
AnalyticsEngineFormatSupport.validateFormat(Format.JDBC);
29+
}
30+
31+
@Test
32+
public void csvFormatIsRejectedAsUnsupportedOperation() {
33+
ErrorReport report =
34+
assertThrows(() -> AnalyticsEngineFormatSupport.validateFormat(Format.CSV));
35+
assertEquals(ErrorCode.UNSUPPORTED_OPERATION, report.getCode());
36+
assertTrue(report.getMessage().toLowerCase().contains("csv"));
37+
assertTrue(report.getMessage().contains("analytics engine"));
38+
assertNotNull(report.getSuggestion());
39+
}
40+
41+
@Test
42+
public void rawFormatIsRejected() {
43+
ErrorReport report =
44+
assertThrows(() -> AnalyticsEngineFormatSupport.validateFormat(Format.RAW));
45+
assertEquals(ErrorCode.UNSUPPORTED_OPERATION, report.getCode());
46+
assertTrue(report.getMessage().toLowerCase().contains("raw"));
47+
}
48+
49+
@Test
50+
public void vizFormatIsRejected() {
51+
ErrorReport report =
52+
assertThrows(() -> AnalyticsEngineFormatSupport.validateFormat(Format.VIZ));
53+
assertEquals(ErrorCode.UNSUPPORTED_OPERATION, report.getCode());
54+
}
55+
56+
private static ErrorReport assertThrows(Runnable runnable) {
57+
try {
58+
runnable.run();
59+
} catch (ErrorReport e) {
60+
return e;
61+
}
62+
throw new AssertionError("Expected ErrorReport to be thrown, but nothing was thrown");
63+
}
64+
}

0 commit comments

Comments
 (0)