Skip to content

Commit 89176d1

Browse files
authored
Simulate API: Return 400 on invalid processor(s) (#130325)
Return a HTTP 400 status code rather than a 500 on missing processor or invalid grok pattern within simulate API Closes #120731
1 parent 0a178b3 commit 89176d1

File tree

7 files changed

+111
-6
lines changed

7 files changed

+111
-6
lines changed

docs/changelog/130325.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
pr: 130325
2+
summary: "Simulate API: Return 400 on invalid processor(s)"
3+
area: Ingest Node
4+
type: bug
5+
issues:
6+
- 120731
7+
breaking:
8+
title: Return 400 on invalid processor(s) in Simulate API
9+
area: Ingest
10+
details: "In earlier versions of {es}, the Simulate API would return a 500 error\
11+
\ when encountering invalid processors. Now, it returns a 400 Bad Request error\
12+
\ instead."
13+
impact: Callers should expect a 400 Bad Request response when the Simulate API encounters
14+
invalid processors. This change improves error handling and provides clearer feedback
15+
on request issues.
16+
notable: false

modules/ingest-common/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ dependencies {
2828

2929
restResources {
3030
restApi {
31-
include '_common', 'ingest', 'cluster', 'indices', 'index', 'bulk', 'nodes', 'get', 'update', 'cat', 'mget', 'search'
31+
include '_common', 'ingest', 'cluster', 'indices', 'index', 'bulk', 'nodes', 'get', 'update', 'cat', 'mget', 'search', 'simulate'
3232
}
3333
}
3434

modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/120_grok.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,48 @@ teardown:
154154
ingest.processor_grok: {}
155155
- length: { patterns: 318 }
156156
- match: { patterns.PATH: "(?:%{UNIXPATH}|%{WINPATH})" }
157+
158+
159+
---
160+
"Test simulate with invalid GROK pattern":
161+
- requires:
162+
cluster_features: [ "simulate.ingest.400_on_failure" ]
163+
reason: "simulate.ingest returned 500 on failure before"
164+
- skip:
165+
features: headers
166+
- do:
167+
catch: bad_request
168+
headers:
169+
Content-Type: application/json
170+
simulate.ingest:
171+
pipeline: "invalid-grok"
172+
body: >
173+
{
174+
"docs": [
175+
{
176+
"_index": "index-1",
177+
"_source": {
178+
"foo": "bar"
179+
}
180+
}
181+
],
182+
"pipeline_substitutions": {
183+
"invalid-grok": {
184+
"description": "invalid grok pattern",
185+
"processors": [
186+
{
187+
"grok": {
188+
"field": "field",
189+
"patterns": [
190+
"%{INVALID_PATTERN:field}"
191+
]
192+
}
193+
}
194+
]
195+
}
196+
}
197+
}
198+
- match: { status: 400 }
199+
- match: { error.reason: "[patterns] Invalid regex pattern found in: [%{INVALID_PATTERN:field}]. Unable to find pattern [INVALID_PATTERN] in Grok's pattern dictionary" }
200+
- match: { error.property_name: "patterns" }
201+
- match: { error.processor_type: "grok" }

rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/simulate.ingest/10_basic.yml

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -600,7 +600,9 @@ setup:
600600

601601
---
602602
"Test bad pipeline substitution":
603-
603+
- requires:
604+
cluster_features: [ "simulate.ingest.400_on_failure" ]
605+
reason: "simulate.ingest returned 500 on failure before"
604606
- skip:
605607
features: [headers, allowed_warnings]
606608

@@ -628,7 +630,7 @@ setup:
628630
default_pipeline: "my-pipeline"
629631

630632
- do:
631-
catch: "request"
633+
catch: bad_request
632634
headers:
633635
Content-Type: application/json
634636
simulate.ingest:
@@ -661,7 +663,8 @@ setup:
661663
}
662664
}
663665
}
664-
- match: { status: 500 }
666+
- match: { status: 400 }
667+
- match: { error.reason: "No processor type exists with name [non-existent-processor]" }
665668

666669
---
667670
"Test index in path":
@@ -751,6 +754,9 @@ setup:
751754

752755
---
753756
"Test simulate with pipeline with created_date":
757+
- requires:
758+
cluster_features: [ "simulate.ingest.400_on_failure" ]
759+
reason: "simulate.ingest returned 500 on failure before"
754760
- requires:
755761
test_runner_features: capabilities
756762
capabilities:
@@ -763,7 +769,7 @@ setup:
763769
- skip:
764770
features: headers
765771
- do:
766-
catch: request
772+
catch: bad_request
767773
headers:
768774
Content-Type: application/json
769775
simulate.ingest:
@@ -785,5 +791,5 @@ setup:
785791
}
786792
}
787793
}
788-
- match: { status: 500 }
794+
- match: { status: 400 }
789795
- contains: { error.reason: "Provided a pipeline property which is managed by the system: created_date." }

server/src/main/java/org/elasticsearch/ingest/IngestFeatures.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import java.util.Set;
1717

1818
public class IngestFeatures implements FeatureSpecification {
19+
private static final NodeFeature SIMULATE_INGEST_400_ON_FAILURE = new NodeFeature("simulate.ingest.400_on_failure", true);
20+
1921
@Override
2022
public Set<NodeFeature> getFeatures() {
2123
if (DataStream.LOGS_STREAM_FEATURE_FLAG) {
@@ -24,4 +26,9 @@ public Set<NodeFeature> getFeatures() {
2426
return Set.of();
2527
}
2628
}
29+
30+
@Override
31+
public Set<NodeFeature> getTestFeatures() {
32+
return Set.of(SIMULATE_INGEST_400_ON_FAILURE);
33+
}
2734
}

server/src/main/java/org/elasticsearch/ingest/SimulateIngestService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
package org.elasticsearch.ingest;
1111

12+
import org.elasticsearch.ElasticsearchException;
1213
import org.elasticsearch.action.bulk.BulkRequest;
1314
import org.elasticsearch.action.bulk.SimulateBulkRequest;
1415
import org.elasticsearch.cluster.metadata.ProjectId;
@@ -28,6 +29,8 @@ public SimulateIngestService(IngestService ingestService, BulkRequest request) {
2829
if (request instanceof SimulateBulkRequest simulateBulkRequest) {
2930
try {
3031
pipelineSubstitutions = getPipelineSubstitutions(simulateBulkRequest.getPipelineSubstitutions(), ingestService);
32+
} catch (ElasticsearchException elasticEx) {
33+
throw elasticEx;
3134
} catch (Exception e) {
3235
throw new RuntimeException(e);
3336
}

server/src/test/java/org/elasticsearch/ingest/SimulateIngestServiceTests.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
package org.elasticsearch.ingest;
1111

12+
import org.elasticsearch.ElasticsearchParseException;
1213
import org.elasticsearch.action.bulk.FailureStoreMetrics;
1314
import org.elasticsearch.action.bulk.SimulateBulkRequest;
1415
import org.elasticsearch.client.internal.Client;
@@ -34,6 +35,7 @@
3435
import static org.elasticsearch.test.LambdaMatchers.transformedMatch;
3536
import static org.hamcrest.Matchers.contains;
3637
import static org.hamcrest.Matchers.equalTo;
38+
import static org.hamcrest.Matchers.is;
3739
import static org.mockito.ArgumentMatchers.anyString;
3840
import static org.mockito.Mockito.mock;
3941
import static org.mockito.Mockito.when;
@@ -118,6 +120,32 @@ public void testGetPipeline() {
118120
}
119121
}
120122

123+
public void testRethrowingOfElasticParseExceptionFromProcessors() {
124+
final PipelineConfiguration pipelineConfiguration = new PipelineConfiguration("pipeline1", new BytesArray("""
125+
{"processors": []}"""), XContentType.JSON);
126+
final IngestMetadata ingestMetadata = new IngestMetadata(Map.of("pipeline1", pipelineConfiguration));
127+
final Processor.Factory factoryThatThrowsElasticParseException = (factory, tag, description, config, projectId) -> {
128+
throw new ElasticsearchParseException("exception to be caught");
129+
};
130+
final Map<String, Processor.Factory> processors = Map.of("parse_exception_processor", factoryThatThrowsElasticParseException);
131+
final var projectId = randomProjectIdOrDefault();
132+
IngestService ingestService = createWithProcessors(projectId, processors);
133+
ingestService.innerUpdatePipelines(projectId, ingestMetadata);
134+
SimulateBulkRequest simulateBulkRequest = new SimulateBulkRequest(
135+
newHashMap("pipeline1", newHashMap("processors", List.of(Map.of("parse_exception_processor", new HashMap<>(0))))),
136+
Map.of(),
137+
Map.of(),
138+
Map.of(),
139+
null
140+
);
141+
142+
final ElasticsearchParseException ex = assertThrows(
143+
ElasticsearchParseException.class,
144+
() -> new SimulateIngestService(ingestService, simulateBulkRequest)
145+
);
146+
assertThat(ex.getMessage(), is("exception to be caught"));
147+
}
148+
121149
private static IngestService createWithProcessors(ProjectId projectId, Map<String, Processor.Factory> processors) {
122150
Client client = mock(Client.class);
123151
ThreadPool threadPool = mock(ThreadPool.class);

0 commit comments

Comments
 (0)