Skip to content

Commit 33f8333

Browse files
authored
Return a 410 (Gone) status code for unavailable API endpoints (#97397)
Updates RestController to return a 410 (Gone) status code for known REST handlers that are unavailable in the current environment (typically serverless). Also uses ElasticsearchException to return the detailed format by default, that contains more useful information for caller-side diagnostics and logs (in particular the "type" property).
1 parent d32dc73 commit 33f8333

File tree

7 files changed

+60
-15
lines changed

7 files changed

+60
-15
lines changed

docs/changelog/97397.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 97397
2+
summary: Return a 410 (Gone) status code for unavailable API endpoints
3+
area: Infra/REST API
4+
type: enhancement
5+
issues: []

server/src/main/java/org/elasticsearch/ElasticsearchException.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.elasticsearch.index.Index;
2929
import org.elasticsearch.index.mapper.DocumentParsingException;
3030
import org.elasticsearch.index.shard.ShardId;
31+
import org.elasticsearch.rest.ApiNotAvailableException;
3132
import org.elasticsearch.rest.RestStatus;
3233
import org.elasticsearch.search.SearchException;
3334
import org.elasticsearch.search.aggregations.MultiBucketConsumerService;
@@ -1844,7 +1845,8 @@ private enum ElasticsearchExceptionHandle {
18441845
ElasticsearchRoleRestrictionException::new,
18451846
170,
18461847
TransportVersion.V_8_500_016
1847-
);
1848+
),
1849+
API_NOT_AVAILABLE_EXCEPTION(ApiNotAvailableException.class, ApiNotAvailableException::new, 171, TransportVersion.V_8_500_065);
18481850

18491851
final Class<? extends ElasticsearchException> exceptionClass;
18501852
final CheckedFunction<StreamInput, ? extends ElasticsearchException, IOException> constructor;

server/src/main/java/org/elasticsearch/TransportVersion.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ private static TransportVersion registerTransportVersion(int id, String uniqueId
178178
public static final TransportVersion V_8_500_062 = registerTransportVersion(8_500_062, "09CD9C9B-3207-4B40-8756-B7A12001A885");
179179
public static final TransportVersion V_8_500_063 = registerTransportVersion(8_500_063, "31dedced-0055-4f34-b952-2f6919be7488");
180180
public static final TransportVersion V_8_500_064 = registerTransportVersion(8_500_064, "3a795175-5e6f-40ff-90fe-5571ea8ab04e");
181+
public static final TransportVersion V_8_500_065 = registerTransportVersion(8_500_065, "4e253c58-1b3d-11ee-be56-0242ac120002");
181182

182183
/*
183184
* STOP! READ THIS FIRST! No, really,
@@ -201,7 +202,7 @@ private static TransportVersion registerTransportVersion(int id, String uniqueId
201202
*/
202203

203204
private static class CurrentHolder {
204-
private static final TransportVersion CURRENT = findCurrent(V_8_500_064);
205+
private static final TransportVersion CURRENT = findCurrent(V_8_500_065);
205206

206207
// finds the pluggable current version, or uses the given fallback
207208
private static TransportVersion findCurrent(TransportVersion fallback) {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.rest;
10+
11+
import org.elasticsearch.ElasticsearchException;
12+
import org.elasticsearch.common.io.stream.StreamInput;
13+
14+
import java.io.IOException;
15+
16+
import static org.elasticsearch.rest.RestStatus.GONE;
17+
18+
/**
19+
* Thrown when an API is not available in the current environment.
20+
*/
21+
public class ApiNotAvailableException extends ElasticsearchException {
22+
23+
public ApiNotAvailableException(String msg, Object... args) {
24+
super(msg, args);
25+
}
26+
27+
public ApiNotAvailableException(StreamInput in) throws IOException {
28+
super(in);
29+
}
30+
31+
@Override
32+
public RestStatus status() {
33+
return GONE;
34+
}
35+
}

server/src/main/java/org/elasticsearch/rest/RestController.java

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@
5757
import static org.elasticsearch.rest.RestStatus.INTERNAL_SERVER_ERROR;
5858
import static org.elasticsearch.rest.RestStatus.METHOD_NOT_ALLOWED;
5959
import static org.elasticsearch.rest.RestStatus.NOT_ACCEPTABLE;
60-
import static org.elasticsearch.rest.RestStatus.NOT_FOUND;
6160
import static org.elasticsearch.rest.RestStatus.OK;
6261

6362
public class RestController implements HttpServerTransport.Dispatcher {
@@ -664,17 +663,8 @@ public static void handleBadRequest(String uri, RestRequest.Method method, RestC
664663

665664
public static void handleServerlessRequestToProtectedResource(String uri, RestRequest.Method method, RestChannel channel)
666665
throws IOException {
667-
try (XContentBuilder builder = channel.newErrorBuilder()) {
668-
builder.startObject();
669-
{
670-
builder.field(
671-
"error",
672-
"uri [" + uri + "] with method [" + method + "] exists but is not available when running in " + "serverless mode"
673-
);
674-
}
675-
builder.endObject();
676-
channel.sendResponse(new RestResponse(NOT_FOUND, builder));
677-
}
666+
String msg = "uri [" + uri + "] with method [" + method + "] exists but is not available when running in serverless mode";
667+
channel.sendResponse(new RestResponse(channel, new ApiNotAvailableException(msg)));
678668
}
679669

680670
/**

server/src/test/java/org/elasticsearch/ExceptionSerializationTests.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
import org.elasticsearch.ingest.IngestProcessorException;
6868
import org.elasticsearch.repositories.RepositoryConflictException;
6969
import org.elasticsearch.repositories.RepositoryException;
70+
import org.elasticsearch.rest.ApiNotAvailableException;
7071
import org.elasticsearch.rest.RestResponseTests;
7172
import org.elasticsearch.rest.RestStatus;
7273
import org.elasticsearch.rest.action.admin.indices.AliasesNotFoundException;
@@ -830,6 +831,7 @@ public void testIds() {
830831
ids.put(168, DocumentParsingException.class);
831832
ids.put(169, HttpHeadersValidationException.class);
832833
ids.put(170, ElasticsearchRoleRestrictionException.class);
834+
ids.put(171, ApiNotAvailableException.class);
833835

834836
Map<Class<? extends ElasticsearchException>, Integer> reverse = new HashMap<>();
835837
for (Map.Entry<Integer, Class<? extends ElasticsearchException>> entry : ids.entrySet()) {

server/src/test/java/org/elasticsearch/rest/RestControllerTests.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.elasticsearch.common.unit.ByteSizeValue;
2424
import org.elasticsearch.common.util.MockPageCacheRecycler;
2525
import org.elasticsearch.common.util.concurrent.ThreadContext;
26+
import org.elasticsearch.common.xcontent.XContentHelper;
2627
import org.elasticsearch.core.IOUtils;
2728
import org.elasticsearch.core.RestApiVersion;
2829
import org.elasticsearch.http.HttpHeadersValidationException;
@@ -939,8 +940,17 @@ public void testApiProtectionWithServerlessEnabledAsEndUser() {
939940
});
940941
final Consumer<List<String>> checkProtected = paths -> paths.forEach(path -> {
941942
RestRequest request = new FakeRestRequest.Builder(xContentRegistry()).withPath(path).build();
942-
AssertingChannel channel = new AssertingChannel(request, false, RestStatus.NOT_FOUND);
943+
AssertingChannel channel = new AssertingChannel(request, true, RestStatus.GONE);
943944
restController.dispatchRequest(request, channel, new ThreadContext(Settings.EMPTY));
945+
946+
RestResponse restResponse = channel.getRestResponse();
947+
Map<String, Object> map = XContentHelper.convertToMap(restResponse.content(), false, XContentType.JSON).v2();
948+
assertEquals(410, map.get("status"));
949+
@SuppressWarnings("unchecked")
950+
Map<String, Object> error = (Map<String, Object>) map.get("error");
951+
assertEquals("api_not_available_exception", error.get("type"));
952+
assertTrue(error.get("reason").toString().contains("not available when running in serverless mode"));
953+
944954
});
945955

946956
List<String> accessiblePaths = List.of("/public", "/internal");

0 commit comments

Comments
 (0)